Packages

Elixometer is a light wrapper around exometer that defines and subscribes metrics automatically to the configured reporter.

Current section

Files

Jump to
elixometer lib elixometer.ex
Raw

lib/elixometer.ex

defmodule Elixometer do
@moduledoc ~S"""
A light wrapper around exometer.
Elixometer allows you to define metrics and subscribe them automatically
to the default reporter for your environment.
## Configuration
In one of your config files, set up an exometer reporter, and then register
it to elixometer like this:
config(:elixometer, reporter: :exometer_report_tty)
## Metrics
Defining metrics in elixometer is substantially easier than in exometer.
Instead of defining and then updating a metric, just update it. Also, instead
of providing a list of atoms, a metric is named with a period separated
bitstring. Presently, Elixometer supports timers, histograms, gauges,
and counters.
Timings may also be defined by annotating a function with a @timed annotation.
This annotation takes a key argument, which tells elixometer what key to use. You
can specify :auto and a key will be generated from the module name and method name.
Updating a metric is similarly easy:
defmodule ParentModule.MetricsTest do
use Elixometer
def counter_test(thingie) do
update_counter("metrics_test.\#{thingie}.count", 1)
end
def timer_test do
timed("metrics_test.timer_test.timings") do
OtherModule.slow_method
end
end
@timed(key: "timed.function")
def function_that_is_timed do
OtherModule.slow_method
end
@timed(key: :auto) # The key will be "parent_module.metrics_test.another_timed_function"
def another_timed_function do
OtherModule.slow_method
end
end
"""
defmodule App do
use Application
def start(_type, _args_) do
Elixometer.Supervisor.start_link
end
end
defmodule Config do
defstruct table_name: nil, counters: Map.new
end
defmodule Timer do
defstruct method_name: nil, key: nil, units: :micros, args: nil, guards: nil, body: nil
end
@elixometer_table :elixometer
alias Elixometer.Updater
import Elixometer.Utils
use GenServer
defmacro __using__(_mod) do
quote do
import Elixometer
Module.register_attribute(__MODULE__, :elixometer_timers, accumulate: true)
@before_compile Elixometer
@on_definition Elixometer
end
end
def __on_definition__(env, _kind, name, args, guards, body) do
mod = env.module
timer_info = Module.get_attribute(mod, :timed)
if timer_info do
key = case timer_info[:key] do
:auto ->
# Convert a fully qualified module to an underscored representation.
# Module.SubModule.SubSubModule will become
# module.sub_module.sub_sub_module
prefix = mod
|> inspect
|> String.replace(~r/([a-z])([A-Z])/, ~S"\1_\2")
|> String.downcase
"#{prefix}.#{name}"
other -> other
end
units = timer_info[:units] || :micros
Module.put_attribute(mod, :elixometer_timers,
%Timer{method_name: name,
args: args,
guards: guards,
body: body,
units: units,
key: key})
Module.delete_attribute(mod, :timed)
end
end
defp build_timer_body(timer_data=%Timer{}) do
quote do
timed(unquote(timer_data.key), unquote(timer_data.units)) do
unquote(timer_data.body)
end
end
end
defmacro __before_compile__(env) do
mod = env.module
timers = Module.get_attribute(mod, :elixometer_timers)
timed_methods = timers
|> Enum.reverse
|> Enum.map(
fn(timer_data=%Timer{}) ->
Module.make_overridable(mod,
[{timer_data.method_name, length(timer_data.args)}])
body = build_timer_body(timer_data)
if length(timer_data.guards) > 0 do
quote do
def unquote(timer_data.method_name)(unquote_splicing(timer_data.args)) when unquote_splicing(timer_data.guards) do
unquote(body)
end
end
else
quote do
def unquote(timer_data.method_name)(unquote_splicing(timer_data.args)) do
unquote(body)
end
end
end
end)
quote do
unquote_splicing(timed_methods)
end
end
def init(:ok) do
table_name = :ets.new(@elixometer_table, [:set, :named_table, read_concurrency: true])
:timer.send_interval(250, :tick)
{:ok, %Config{table_name: table_name}}
end
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def get_metric_value(metric_name) do
metric_name
|> to_atom_list
|> :exometer.get_value
end
def get_metric_value(metric_name, data_point) do
metric_val = metric_name
|> to_atom_list
|> :exometer.get_value(data_point)
case metric_val do
{:ok, metric} -> {:ok, metric[data_point]}
r = {:error, _reason} -> r
end
end
@doc """
Updates a histogram with a new value. If the metric doesn't exist, a new metric
is created and subscribed to.
"""
def update_histogram(name, delta, aggregate_seconds\\60) when is_bitstring(name) do
Updater.histogram(name, delta, aggregate_seconds)
end
@doc """
Updates and alternately creates spiral metric. A spiral metric is a metric maintains a series
of internal slots that 'age out' and are replaced by newer values. This is useful for
maintaining QPS stats.
"""
def update_spiral(name, delta, opts \\ [time_span: :timer.seconds(60), slot_period: 1000]) do
Updater.spiral(name, delta, opts)
end
@doc """
Updates a counter metric. If the metric doesn't exist, the metric is created
and the metric is subscribed to the default reporter.
If the value of the `:reset_seconds` option is greater than zero, the counter will be reset
automatically at the specified interval.
"""
def update_counter(name, delta, [reset_seconds: secs] \\ [reset_seconds: nil]) when is_bitstring(name) and (is_nil(secs) or secs >= 1) do
Updater.counter(name, delta, secs)
end
@doc """
Clears a counter with the given name.
"""
def clear_counter(metric_name) when is_bitstring(metric_name) do
clear_counter(name_to_exometer(:counters, metric_name))
end
def clear_counter(metric_name) when is_list(metric_name) do
:exometer.reset(metric_name)
end
@doc """
Updates a gauge metric. If the metric doesn't exist, the metric is created
and the metric is subscribed to the default reporter.
"""
def update_gauge(name, value) when is_bitstring(name) do
Updater.gauge(name, value)
end
@doc """
Updates a timer metric. If the metric doesn't exist, it will be created and
subscribed to the default reporter.
The time units default to *microseconds*, but you can pass in a unit of
:millis and the value will be converted.
"""
defmacro timed(name, units \\ :micros, do: block) do
converted_name = Elixometer.Utils.name_to_exometer(:timers, name)
quote do
{elapsed_us, rv} = :timer.tc(fn -> unquote(block) end)
Updater.timer(unquote(converted_name), unquote(units), elapsed_us)
rv
end
end
def add_counter(metric_name, ttl_millis) do
GenServer.cast(__MODULE__, {:add_counter, metric_name, ttl_millis})
end
def add_counter(metric_name) do
GenServer.cast(__MODULE__, {:add_counter, metric_name, nil})
end
def metric_defined?(name) when is_bitstring(name) do
name |> to_atom_list |> metric_defined?
end
def metric_defined?(name) do
:ets.member(@elixometer_table, {:definitions, name})
end
def metric_subscribed?(name) do
:ets.member(@elixometer_table, {:subscriptions, name})
end
def ensure_subscribed(name) do
if not metric_subscribed?(name) do
GenServer.call(__MODULE__, {:subscribe, name})
end
end
def ensure_metric_defined(name, defn_fn) do
if not metric_defined?(name) do
GenServer.call(__MODULE__, {:define_metric, name, defn_fn})
end
:ok
end
@doc """
Ensures a metric is correctly registered in Elixometer.
This means that Elixometer knows about it and its metrics are
subscribed to an exometer reporter
"""
def ensure_registered(metric_name, register_fn) do
try do
ensure_metric_defined(metric_name, register_fn)
subscribe(metric_name)
rescue
e in ErlangError -> e
end
end
@doc """
Ensures that a metric is subscribed to an exometer reporter.
"""
def subscribe(metric_name) do
if not metric_subscribed?(metric_name) do
GenServer.call(__MODULE__, {:subscribe, metric_name})
end
end
def handle_call({:subscribe, metric_name}, _caller, state) do
create_subscription(metric_name)
{:reply, :ok, state}
end
def handle_call({:define_metric, metric_name, defn_fn}, _caller, state) do
# we re-check whether the metric is defined here to prevent
# a race condition in ensure_metric_defined
if not metric_defined?(metric_name) do
defn_fn.()
:ets.insert(@elixometer_table, {{:definitions, metric_name}, true})
end
{:reply, :ok, state}
end
def handle_cast({:add_counter, metric_name, ttl_millis}, config) do
new_counters = Map.put(config.counters, metric_name, ttl_millis)
{:noreply, %Config{config | counters: new_counters}}
end
def handle_info(:tick, config) do
Enum.map(config.counters,
fn({name, millis}) ->
{:ok, [ms_since_reset: since_reset]} = :exometer.get_value(name, :ms_since_reset)
if millis && since_reset >= millis do
:exometer.reset(name)
end
end)
{:noreply, config}
end
defp create_subscription(metric_name) do
# If a metric isn't subscribed to our reporters, create a subscription in our
# ets table and subscribe our metric to exometer's reporters.
if not metric_subscribed?(metric_name) do
cfg = Application.get_all_env(:elixometer)
reporter = cfg[:reporter]
interval = cfg[:update_frequency]
if reporter do
:exometer.info(metric_name)
|> Keyword.get(:datapoints)
|> Enum.map(&(:exometer_report.subscribe(reporter, metric_name, &1, interval)))
end
:ets.insert(@elixometer_table, {{:subscriptions, metric_name}, true})
end
end
end