Current section

Files

Jump to
metrics_reader src metrics_observer.erl
Raw

src/metrics_observer.erl

%% @doc Observing a metric involves periodically recording a snapshot of
%% a scalar metric in the form of a histogram. These snapshots are recorded
%% over a pre-configured interval. Each histogram will be registered
%% automatically with the `metrics_reader'.
%% Currently, only the folsom backend is supported but it is possible to extend
%% the idea to any metrics backend using the erlang-metrics library
%% interface here:
%% https://github.com/benoitc/erlang-metrics
%% Warning: observing a metric will clear it's current value whenenver a
%% snapshot is recorded.
%% @end
-module(metrics_observer).
-behaviour(gen_server).
-include("metrics_reader.hrl").
%% API
-export([start_link/0,
observe/2,
unobserve/1,
observed/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {running = false :: boolean(),
observations = sets:new(),
slide_interval :: pos_integer(),
acc_interval :: pos_integer(),
timer_ref :: reference()}).
-type metric_name() :: any().
-type histogram_name() :: any().
-type state() :: #state{}.
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link() -> gen_server_startlink_ret().
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec observe(metric_name(), histogram_name()) -> ok.
observe(Name, HistogramName) ->
gen_server:call(?SERVER, {observe, Name, HistogramName}).
-spec unobserve(metric_name()) -> ok.
unobserve(Name) ->
gen_server:call(?SERVER, {unobserve, Name}).
-spec observed() -> list().
observed() ->
gen_server:call(?SERVER, observed).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
-spec init([]) -> {ok, state()}.
init([]) ->
%% We want a high priority to ensure reporting accuracy
process_flag(priority, high),
SInterval = metrics_reader_helper:opt(histogram_slide_interval_sec, 60),
AInterval = metrics_reader_helper:opt(histogram_acc_interval_sec, 1),
{ok, #state{slide_interval = SInterval,
acc_interval = AInterval * 1000}}.
-spec handle_call(any(), any(), state()) -> {reply, term(), state()}.
handle_call({observe, Name, HistogramName}, _From,
State = #state{running = false, acc_interval = AccInterval}) ->
State1 = do_observe({Name, HistogramName}, State),
TRef = erlang:send_after(AccInterval, self(), tick),
Reply = ok,
{reply, Reply, State1#state{running = true, timer_ref = TRef}};
handle_call({observe, Name, HistogramName}, _From,
State = #state{running = true}) ->
Reply = ok,
State1 = do_observe({Name, HistogramName}, State),
{reply, Reply, State1};
handle_call({unobserve, Name}, _From,
State = #state{running = true,
timer_ref = TRef,
observations = OSet}) ->
Empty = sets:new(),
[{Name, Histogram}] = [{N, H} || {N, H} <- sets:to_list(OSet), N =:= Name],
State1 = do_unobserve({Name, Histogram}, State),
#state{observations = OSet1} = State1,
case OSet1 of
Empty ->
erlang:cancel_timer(TRef),
{reply, ok, State1#state{running = false, timer_ref = undefined}};
_ ->
{reply, ok, State1}
end;
handle_call(observed, _From, State = #state{observations = OSet}) ->
Reply = [Name || {Name, _HistogramName} <- sets:to_list(OSet)],
{reply, Reply, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
-spec handle_cast(any(), state()) -> {noreply, state()}.
handle_cast(_Msg, State) ->
{noreply, State}.
-spec handle_info(any(), state()) -> {noreply, state()} |
{stop, any(), state()}.
handle_info(tick, State = #state{running = true,
observations = OSet,
acc_interval = AccInterval}) ->
lists:foreach(fun ({Name, HistogramName}) ->
Value = folsom_metrics_counter:get_value(Name),
folsom_metrics:notify({HistogramName, Value}),
folsom_metrics_counter:clear(Name)
end, sets:to_list(OSet)),
TRef = erlang:send_after(AccInterval, self(), tick),
{noreply, State#state{timer_ref = TRef}};
handle_info(_Info, State) ->
{noreply, State}.
-spec terminate(any(), any()) -> ok.
terminate(_Reason, State = #state{timer_ref = TRef,
observations = OSet}) ->
Empty = sets:new(),
#state{observations = Empty} = lists:foldl(
fun do_unobserve/2,
State,
sets:to_list(OSet)),
erlang:cancel_timer(TRef),
ok.
-spec code_change(any(), state(), any()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
do_observe({Name, HistogramName},
State = #state{slide_interval = SlideInterval,
observations = OSet}) ->
case folsom_metrics:get_metric_info(Name) of
[{_, [{type, histogram}]}] ->
ok;
_ ->
folsom_metrics:new_histogram(HistogramName, slide, SlideInterval)
end,
metrics_reader:register(HistogramName),
OSet1 = sets:add_element({Name, HistogramName}, OSet),
State#state{observations = OSet1}.
do_unobserve({Name, HistogramName},
State = #state{observations = OSet}) ->
metrics_reader:deregister(HistogramName),
folsom_metrics:delete_metric(HistogramName),
OSet1 = sets:del_element({Name, HistogramName}, OSet),
State#state{observations = OSet1}.