%% -------------------------------------------------------------------
%%
%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved.
%%
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%%
%% -------------------------------------------------------------------
%% @doc API and behaviour for metrics instances
%%
%% <h2>Predefined templates</h2>
%%
%% It is possible to define a set of defaults for exometer.
%%
%% Example: Putting the following in a sys.config file,
%% <pre lang="erlang">
%% {exometer, [
%% {defaults,
%% [{['_'], function , [{module, exometer_function}]},
%% {['_'], counter , [{module, exometer}]},
%% {['_'], fast_counter, [{module, exometer}]},
%% {['_'], gauge , [{module, exometer}]},
%% {['_'], histogram , [{module, exometer_histogram}]},
%% {['_'], spiral , [{module, exometer_spiral}]}
%% ]}
%% ]}
%% </pre>
%% will define global defaults for the given metric types. The format is
%% `{NamePattern, Type, Options}'
%%
%% The options can be overridden by options given in the `new()' command.
%%
%% `NamePattern' is similar to that used in {@link find_entries/1}.
%% For more information, see {@link exometer_admin:set_default/3}.
%% @end
-module(exometer).
-export(
[
new/2, new/3,
re_register/3, ensure/3,
repair/1,
propose/3,
update/2, update_or_create/2, update_or_create/4,
get_value/1, get_value/2, get_values/1,
sample/1,
delete/1,
reset/1,
setopts/2,
find_entries/1,
select/1, select/2, select_cont/1, select_count/1,
aggregate/2,
info/1, info/2,
register_application/0,
register_application/1
]).
-export([global_status/1]).
-export([create_entry/1]). % called only from exometer_admin.erl
%% Convenience function for testing
-export([start/0, stop/0]).
-export_type([name/0, type/0, options/0, status/0, behaviour/0,
entry/0, datapoint/0]).
-compile(inline).
-include("exometer.hrl").
-type name() :: list().
-type type() :: atom()
| {function, M :: atom(), F :: atom()}
| {function, M :: atom(), F :: atom(), ArgSpec :: list(),
Type :: atom(), DataPoints :: list()}
| {Type :: atom(), Arg :: any()}.
-type status() :: enabled | disabled.
-type options() :: [{atom(), any()}].
-type value() :: any().
-type error() :: {error, any()}.
-type behaviour() :: probe | entry.
-type entry() :: #exometer_entry{}.
-type datapoint() :: atom() | integer().
-define(IS_ENABLED(St), St==enabled orelse St band 2#1 == 1).
-define(IS_DISABLED(St), St==disabled orelse St band 2#1 =/= 1).
-define(EVENT_ENABLED(St), St band 2#10 == 2#10).
%% @doc Start exometer and dependent apps (for testing).
start() ->
{ok,_} = exometer_util:ensure_all_started(exometer_core),
ok.
%% @doc Stop exometer and dependent apps (for testing).
stop() ->
application:stop(exometer_core).
-spec new(name(), type()) -> ok.
%% @equiv new(Name, Type, [])
new(Name, Type) ->
new(Name, Type, []).
-spec new(name(), type(), options()) -> ok.
%% @doc Create a new metrics entry.
%%
%% `Name' must be a list of terms (e.g. atoms). `Type' must be either one
%% of the built-in types, or match a predefined template.
%%
%% `Options' will be passed to the entry, but the framework will recognize
%% the following options:
%%
%% * `{cache, Lifetime}' - Cache the results of {@link get_value/1} for
%% the given number of milliseconds. Subsequent calls to {@link get_value/1}
%% will get the cached value, if found. Default is `0', which means no
%% caching will be performed.
%%
%% * `{status, enabled | disabled}' - Default is `enabled'. If the metric
%% is `disabled', calls to {@link get_value/1} will return `{ok, disabled}',
%% and calls to {@link update/2} and {@link sample/1} will return `ok' but
%% will do nothing.
%%
%% * `{snmp, [{DataPoint, ReportInterval}]}' - defines a link to SNMP reporting,
%% where the given data points are sampled at the given intervals, converted
%% to SNMP PDUs and transmitted via the `exometer_report_snmp' reporter.
%%
%% * `{snmp_syntax, [{DataPoint | {default}, SYNTAX}]}' - specifies a custom
%% SNMP type for a given data point. `SYNTAX' needs to be a binary or a string,
%% and corresponds to the SYNTAX definition in the generated SNMP MIB.
%%
%% * `{aliases, [{DataPoint, Alias}]}' - maps aliases to datapoints.
%% See {@link exometer_alias:new/2}.
%%
%% * <code>{'--', Keys}</code> removes option keys from the applied template.
%% This can be used to clean up the options list when overriding the defaults
%% for a given namespace (if the default definition contains options that are
%% not applicable, or would even cause problems with the current entry.)
%%
%% For example, the default value for an exometer counter is `"Counter32"', which
%% expands to `SYNTAX Counter32' in the corresponding MIB object definition. If
%% a 64-bit counter (not supported by SNMPv1) is desired instead, the option
%% `{snmp_syntax, [{value, "Counter64"}]}' can be added to the counter entry
%% (note that `value' in this case is the name of the data point representing
%% the counter value).
%%
%% @end
new(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
exometer_admin:new_entry(Name, Type, Opts).
-spec propose(name(), type(), options()) -> exometer_info:pp() | error().
%% @doc Propose a new exometer entry (no entry actually created).
%%
%% This function analyzes a proposed entry definition, applying templates
%% and processing options in the same way as {@link new/3}, but not actually
%% creating the entry. The return value, if successful, corresponds to
%% `exometer_info:pp(Entry)'.
%% @end
propose(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
exometer_admin:propose(Name, Type, Opts).
-spec re_register(name(), type(), options()) -> ok.
%% @doc Create a new metrics entry, overwrite any old entry.
%%
%% This function behaves as {@link new/3}, but will not fail if an entry
%% with the same name already exists. Instead, the old entry will be replaced
%% by the new.
%% @end
re_register(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
exometer_admin:re_register_entry(Name, Type, Opts).
-spec repair(name()) -> ok.
%% @doc Delete and re-create an entry.
%%
%% This function can be tried if a metric (e.g. a complex probe) has become
%% 'stuck' or otherwise isn't functioning properly. It fetches the stored
%% meta-data and then deletes and re-creates the metric.
%% @end
repair(Name) ->
exometer_admin:repair_entry(Name).
-spec ensure(name(), type(), options()) -> ok | error().
%% @doc Ensure that metric exists and is of given type.
%%
%% This function is similar to re-register, but doesn't actually re-register
%% a metric if it already exists. If a matching entry is found, a check is
%% performed to verify that it is of the correct type. If it isn't, an
%% error tuple is returned.
%% @end
ensure(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
exometer_admin:ensure(Name, Type, Opts).
-dialyzer({no_match, update/2}).
-spec update(name(), value()) -> ok | error().
%% @doc Update the given metric with `Value'.
%%
%% The exact semantics of an update will vary depending on metric type.
%% For exometer's built-in counters, the counter instance on the current
%% scheduler will be incremented. For a plugin metric (e.g. a probe), the
%% corresponding callback module will be called. For a disabled metric,
%% `ok' will be returned without any other action being taken.
%% @end
update(Name, Value) ->
case exometer_global:status() of
enabled -> update_(Name, Value);
_ -> ok
end.
update_(Name, Value) when is_list(Name) ->
case ets:lookup(Table = exometer_util:table(), Name) of
[#exometer_entry{status = Status} = E]
when ?IS_ENABLED(Status) ->
case E of
#exometer_entry{module = ?MODULE, type = counter} ->
ets:update_counter(
Table, Name, {#exometer_entry.value, Value});
#exometer_entry{module = ?MODULE,
type = fast_counter, ref = {M, F}} ->
fast_incr(Value, M, F);
#exometer_entry{module = ?MODULE, type = gauge} ->
ets:update_element(
?EXOMETER_ENTRIES,
Name, [{#exometer_entry.value, Value}]);
#exometer_entry{behaviour = probe,
type = T, ref = Pid} ->
exometer_probe:update(Name, Value, T, Pid);
#exometer_entry{module = M, behaviour = entry,
type = Type, ref = Ref} ->
M:update(Name, Value, Type, Ref)
end,
update_ok(Status, E, Value);
[] ->
{error, not_found};
_ ->
ok
end.
update_ok(St, #exometer_entry{name = Name}, Value)
when St band 2#10 =:= 2#10 ->
try begin
exometer_event ! {updated, Name, Value},
ok
end
catch
_:_ -> ok
end;
update_ok(_, _, _) ->
ok.
-spec update_or_create(Name::name(), Value::value()) -> ok | error().
%% @doc Update existing metric, or create+update according to template.
%%
%% If the metric exists, it is updated (see {@link update/2}). If it doesn't,
%% exometer searches for a template matching `Name', picks the best
%% match and creates a new entry based on the template
%% (see {@link exometer_admin:set_default/3}). Note that fully wild-carded
%% templates (i.e. <code>['_']</code>) are ignored.
%% @end
update_or_create(Name, Value) ->
case update(Name, Value) of
{error, not_found} ->
case exometer_admin:auto_create_entry(Name) of
ok ->
update(Name, Value);
Error ->
Error
end;
ok ->
ok
end.
update_or_create(Name, Value, Type, Opts) ->
case update(Name, Value) of
{error, not_found} ->
ensure(Name, Type, Opts),
update(Name, Value);
ok ->
ok
end.
fast_incr(N, M, F) when N > 0 ->
M:F(),
fast_incr(N-1, M, F);
fast_incr(0, _, _) ->
ok.
%% @doc Fetch the current value of the metric.
%%
%% For a built-in counter, the value returned is the sum of all counter
%% instances (one per scheduler). For plugin metrics, the callback module is
%% responsible for providing the value. If the metric has a specified
%% (non-zero) cache lifetime, and a value resides in the cache, the cached
%% value will be returned.
%% @end
-spec get_value(name()) -> {ok, value()} | {error, not_found}.
get_value(Name) when is_list(Name) ->
get_value(Name, default).
-spec get_value(name(), datapoint() | [datapoint()]) ->
{ok, value()} | {error, not_found}.
get_value(Name, DataPoint) when is_list(Name), is_integer(DataPoint) ->
get_value(Name, [DataPoint]);
get_value(Name, DataPoint) when is_list(Name), is_atom(DataPoint),
DataPoint=/=default ->
get_value(Name, [DataPoint]);
%% Also covers DataPoints = default
get_value(Name, DataPoints) when is_list(Name) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{} = E] ->
{ok, get_value_(E, DataPoints)};
_ ->
{error, not_found}
end.
%% If the entry is disabled, just err out.
get_value_(#exometer_entry{ status = Status }, _DataPoints)
when ?IS_DISABLED(Status) ->
disabled;
%% If the value is cached, see if we can find it.
%% In the default case, call again with resolved data points.
get_value_(#exometer_entry{cache = Cache } = E,
DataPoints) when Cache =/= 0 ->
get_cached_value_(E, DataPoints);
get_value_(#exometer_entry{ module = ?MODULE,
type = Type} = E, default)
when Type == counter; Type == fast_counter; Type == gauge ->
get_value_(E, exometer_util:get_datapoints(E));
get_value_(#exometer_entry{ module = ?MODULE,
type = counter} = E, DataPoints0) ->
DataPoints = datapoints(DataPoints0, E),
[get_ctr_datapoint(E, D) || D <- DataPoints];
get_value_(#exometer_entry{ module = ?MODULE,
type = gauge, name = Name}, DataPoints0) ->
[E] = ets:lookup(?EXOMETER_ENTRIES, Name),
DataPoints = datapoints(DataPoints0, E),
[get_gauge_datapoint(E, D) || D <- DataPoints];
get_value_(#exometer_entry{module = ?MODULE,
type = fast_counter} = E, DataPoints0) ->
DataPoints = datapoints(DataPoints0, E),
[get_fctr_datapoint(E, D) || D <- DataPoints ];
get_value_(#exometer_entry{behaviour = entry,
module = Mod,
name = Name,
type = Type,
ref = Ref} = E, DataPoints0) ->
Mod:get_value(Name, Type, Ref, datapoints(DataPoints0, E));
get_value_(#exometer_entry{behaviour = probe,
name = Name,
type = Type,
ref = Ref} = E, DataPoints0) ->
exometer_probe:get_value(Name, Type, Ref, datapoints(DataPoints0, E)).
get_cached_value_(E, default) ->
get_cached_value_(E, exometer_util:get_datapoints(E));
get_cached_value_(#exometer_entry{name = Name,
cache = CacheTTL } = E,
DataPoints) ->
%% Dig through all the data points and check for cache hit.
%% Store all cached KV pairs, and all keys that must be
%% read and cached.
{ Cached, Uncached } =
lists:foldr(fun(DataPoint, {Cached1, Uncached1}) ->
case exometer_cache:read(Name, DataPoint) of
not_found ->
{ Cached1, [DataPoint | Uncached1] };
{_, Value } ->
{ [{ DataPoint, Value } | Cached1], Uncached1 }
end
end, {[],[]}, DataPoints),
if Uncached == [] ->
%% We are done, return
Cached;
true ->
%% Go through all cache misses and retreive their actual values.
case get_value_(E#exometer_entry { cache = 0 }, Uncached) of
{error, unavailable} = Result ->
%% a function entry returns this in exception case.
%% see `exometer_function:get_value/4`
Result;
Result ->
%% Update the cache with all the shiny new values retrieved.
[ exometer_cache:write(Name, DataPoint1, Value1, CacheTTL)
|| { DataPoint1, Value1 } <- Result],
All = Result ++ Cached,
[{_,_} = lists:keyfind(DP, 1, All) || DP <- DataPoints,
lists:keymember(DP,1,All)]
end
end.
-spec delete(name()) -> ok | error().
%% @doc Delete the metric
delete(Name) when is_list(Name) ->
exometer_admin:delete_entry(Name).
-spec sample(name()) -> ok | error().
%% @doc Tells the metric (mainly probes) to take a sample.
%%
%% Probes often take care of data sampling using a configured sample
%% interval. This function provides a way to explicitly tell a probe to
%% take a sample. The operation is asynchronous. For other metrics, the
%% operation likely has no effect, and will return `ok'.
%% @end
sample(Name) when is_list(Name) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
case E of
#exometer_entry{module = ?MODULE, type = counter} -> ok;
#exometer_entry{module = ?MODULE, type = fast_counter} -> ok;
#exometer_entry{behaviour = probe,
type = Type,
ref = Ref} ->
exometer_probe:sample(Name, Type, Ref),
ok;
#exometer_entry{behaviour = entry,
module = M, type = Type, ref = Ref} ->
M:sample(Name, Type, Ref)
end;
[_] -> disabled;
[] ->
{error, not_found}
end.
-dialyzer({no_match, reset/1}).
-spec reset(name()) -> ok | error().
%% @doc Reset the metric.
%%
%% For a built-in counter, the value of the counter is set to zero. For other
%% types of metric, the callback module will define exactly what happens
%% when a reset() is requested. A timestamp (`os:timestamp()') is saved in
%% the exometer entry, which can be recalled using {@link info/2}, and will
%% indicate the time that has passed since the metric was last reset.
%% @end
reset(Name) ->
case exometer_global:status() of
enabled -> reset_(Name);
_ -> ok
end.
reset_(Name) when is_list(Name) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
case E of
#exometer_entry{module = ?MODULE, type = counter} ->
TS = exometer_util:timestamp(),
[ets:update_element(T, Name, [{#exometer_entry.value, 0},
{#exometer_entry.timestamp, TS}])
|| T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
#exometer_entry{module = ?MODULE, type = fast_counter,
ref = {M, F}} ->
TS = exometer_util:timestamp(),
set_call_count(M, F, true),
[ets:update_element(T, Name, [{#exometer_entry.timestamp, TS}])
|| T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
#exometer_entry{module = ?MODULE, type = gauge} ->
TS = exometer_util:timestamp(),
[ets:update_element(T, Name, [{#exometer_entry.value, 0},
{#exometer_entry.timestamp, TS}])
|| T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
#exometer_entry{behaviour = probe, type = Type, ref = Ref} ->
[exometer_cache:delete(Name, DataPoint) ||
DataPoint <- exometer_util:get_datapoints(E)],
exometer_probe:reset(Name, Type, Ref),
ok;
#exometer_entry{behaviour = entry,
module = M, type = Type, ref = Ref} ->
[exometer_cache:delete(Name, DataPoint) ||
DataPoint <- exometer_util:get_datapoints(E)],
M:reset(Name, Type, Ref)
end;
[] ->
{error, not_found};
_ ->
ok
end.
-spec setopts(name(), options()) -> ok | error().
%% @doc Change options for the metric.
%%
%% Valid options are whatever the metric type supports, plus:
%%
%% * `{cache, Lifetime}' - The cache lifetime (0 for no caching).
%%
%% * `{status, enabled | disabled}' - the operational status of the metric.
%%
%%
%% Note that if the metric is disabled, setopts/2 will fail unless the options
%% list contains `{status, enabled}', which will enable the metric and cause
%% other options to be processed.
%% @end
setopts(Name, Options) when is_list(Name), is_list(Options) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{module = ?MODULE,
type = Type,
status = Status} = E] ->
if ?IS_DISABLED(Status) ->
case lists:keyfind(status, 1, Options) of
{_, enabled} ->
case Type of
fast_counter -> setopts_fctr(E, Options);
counter -> setopts_ctr(E, Options);
gauge -> setopts_gauge(E, Options)
end,
reporter_setopts(E, Options, enabled);
_ ->
{error, disabled}
end;
?IS_ENABLED(Status) ->
case lists:keyfind(status, 1, Options) of
{_, disabled} ->
{_, Elems} = process_setopts(E, Options),
update_entry_elems(Name, Elems),
reporter_setopts(E, Options, disabled);
R when R==false; R=={status,disabled} ->
case Type of
fast_counter -> setopts_fctr(E, Options);
counter -> setopts_ctr(E, Options);
gauge -> setopts_gauge(E, Options)
end,
reporter_setopts(E, Options, enabled)
end
end;
[#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
NewStatus = proplists:get_value(status, Options, enabled),
{_, Elems} = process_setopts(E, Options),
update_entry_elems(Name, Elems),
module_setopts(E, Options, NewStatus);
[#exometer_entry{status = Status} = E] when ?IS_DISABLED(Status) ->
case lists:keyfind(status, 1, Options) of
{_, enabled} ->
{_, Elems} = process_setopts(E, Options),
update_entry_elems(Name, Elems),
module_setopts(E, Options, enabled);
false ->
{error, disabled}
end;
[] ->
{error, not_found}
end.
module_setopts(#exometer_entry{behaviour = probe,
name = Name,
status = St0} = E, Options, NewStatus) ->
reporter_setopts(E, Options, NewStatus),
case NewStatus of
disabled when ?IS_ENABLED(St0) ->
exometer_cache:delete_name(Name),
exometer_probe:stop_probe(E),
update_entry_elems(Name, [{#exometer_entry.ref, undefined}]);
enabled when ?IS_DISABLED(St0) ->
%% Previously, probes were not stopped when disabled
OldRef = E#exometer_entry.ref,
case is_pid(OldRef) andalso is_process_alive(OldRef) of
true -> ok;
false ->
{ok, Ref} = exometer_probe:start_probe(E),
update_entry_elems(Name, [{#exometer_entry.ref, Ref}])
end;
_ ->
exometer_probe:setopts(E, Options, NewStatus)
end,
ok;
module_setopts(#exometer_entry{behaviour = entry,
module=M} = E, Options, NewStatus) ->
case [O || {K, _} = O <- Options,
not lists:member(K, [status, cache, ref])] of
[] ->
ok;
[_|_] = UserOpts ->
case M:setopts(E, UserOpts, NewStatus) of
ok ->
reporter_setopts(E, Options, NewStatus),
ok;
{error,_} = Error ->
Error
end
end.
reporter_setopts(#exometer_entry{} = E, Options, Status) ->
exometer_report:setopts(E, Options, Status).
setopts_fctr(#exometer_entry{name = Name,
ref = OldRef,
status = OldStatus} = E, Options) ->
{#exometer_entry{status = NewStatus}, Elems} =
process_setopts(E, Options),
Ref = case lists:keyfind(function, 1, Options) of
{_, {M, F} = NewRef} when is_atom(M), is_atom(F),
M =/= '_', F =/= '_' -> NewRef;
false -> OldRef
end,
if Ref =/= OldRef ->
set_call_count(OldRef, false),
set_call_count(Ref, NewStatus == enabled);
true ->
if OldStatus =/= NewStatus ->
set_call_count(Ref, NewStatus == enabled);
true ->
%% Setting call_count again will reset the counter
%% so don't do it unnecessarily
ok
end
end,
Elems1 = add_elem(ref, Ref, Elems),
update_entry_elems(Name, Elems1),
ok.
setopts_ctr(#exometer_entry{name = Name} = E, Options) ->
{_, Elems} = process_setopts(E, Options),
update_entry_elems(Name, Elems),
ok.
setopts_gauge(E, Options) ->
setopts_ctr(E, Options). % same logic as for counter
%% cache(0, _, Value) ->
%% Value;
%% cache(TTL, Name, Value) when TTL > 0 ->
%% exometer_cache:write(Name, Value, TTL),
%% Value.
update_entry_elems(Name, Elems) ->
[ets:update_element(T, Name, Elems) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok.
-type info() :: name | type | module | value | cache
| status | timestamp | options | ref | datapoints | entry.
-spec info(name(), info()) -> any().
%% @doc Retrieves information about a metric.
%%
%% Supported info items:
%%
%% * `name' - The name of the metric
%% * `type' - The type of the metric
%% * `module' - The callback module used
%% * `value' - The result of `get_value(Name)'
%% * `cache' - The cache lifetime
%% * `status' - Operational status: `enabled' or `disabled'
%% * `timestamp' - When the metric was last reset/initiated
%% * `datapoints' - Data points available for retrieval with get_value()
%% * `options' - Options passed to the metric at creation (or via setopts())
%% * `ref' - Instance-specific reference; usually a pid (probe) or undefined
%% @end
info(#exometer_entry{status = Status} = E, Item) ->
info_(E, Item, info_status(Status));
info(Name, Item) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{status = Status0} = E] ->
Status = info_status(Status0),
info_(E, Item, Status);
_ ->
undefined
end.
info_(E, Item, Status) ->
case {Item, Status} of
{name, _} -> E#exometer_entry.name;
{type, _} -> E#exometer_entry.type;
{module, _} -> E#exometer_entry.module;
{value, enabled} -> get_value_(E, exometer_util:get_datapoints(E));
{cache, enabled} -> E#exometer_entry.cache;
{status, _} -> Status;
{timestamp, enabled} -> E#exometer_entry.timestamp;
{options, _} -> E#exometer_entry.options;
{ref, _} -> E#exometer_entry.ref;
{entry, _} -> E;
{datapoints, enabled} -> exometer_util:get_datapoints(E);
_ -> undefined
end.
info_status(S) when ?IS_ENABLED(S) ->
enabled;
info_status(_) ->
disabled.
datapoints(default, _E) ->
default;
datapoints(D, _) when is_atom(D) ->
[D];
datapoints(D, _) when is_integer(D) ->
[D];
datapoints(D, _) when is_list(D) ->
D.
-spec info(name()) -> [{info(), any()}] | undefined.
%% @doc Returns a list of info items for Metric, see {@link info/2}.
info(Name) ->
case ets:lookup(exometer_util:table(), Name) of
[#exometer_entry{status = Status0} = E] ->
Status = info_status(Status0),
Flds = record_info(fields, exometer_entry),
lists:map(fun(Item) -> {Item, info_(E, Item, Status)} end, Flds);
_ ->
undefined
end.
-spec find_entries([any() | '_']) -> [{name(), type(), status()}].
%% @doc Find metrics based on a name prefix pattern.
%%
%% This function will find and return metrics whose name matches the given
%% prefix. For example `[kvdb, kvdb_conf, Table]' would match any metrics
%% tied to the given table in the `kvdb_conf' database.
%%
%% It is possible to insert wildcards:
%% <code>[kvdb, kvdb_conf, '_', write]</code> would match
%% `write'-related metrics in all tables of the `kvdb_conf' database.
%%
%% The format of the returned metrics is `[{Name, Type, Status}]'.
%% @end
find_entries(Path) ->
Pat = Path ++ '_',
ets:select(?EXOMETER_ENTRIES,
[ { #exometer_entry{name = Pat, _ = '_'}, [],
[{{ {element, #exometer_entry.name, '$_'},
{element, #exometer_entry.type, '$_'},
status_body_pattern()
}}] } ]).
get_values(Path) ->
Entries = find_entries(Path),
lists:foldr(
fun({Name, _Type, enabled}, Acc) ->
case get_value(Name) of
{ok, V} ->
[{Name, V}|Acc];
{error,not_found} ->
Acc
end
end, [], Entries).
-spec select(ets:match_spec()) -> list().
%% @doc Perform an `ets:select()' on the set of metrics.
%%
%% This function operates on a virtual structure representing the metrics,
%% but otherwise works as a normal `select()'. The representation of the
%% metrics is `{Name, Type, Status}'.
%% @end
select(Pattern) ->
ets:select(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern]).
-spec select_count(ets:match_spec()) -> non_neg_integer().
%% @doc Corresponds to {@link ets:select_count/1}.
%% @end
select_count(Pattern) ->
ets:select_count(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern]).
-spec select(ets:match_spec(), pos_integer() | infinity) -> {list(), _Cont}.
%% @doc Perform an `ets:select()' with a Limit on the set of metrics.
%%
%% This function is equivalent to {@link select/1}, but also takes a limit.
%% After `Limit' number of matches, the function returns the matches and a
%% continuation, which can be passed to {@link select_cont/1}.
%% @end
select(Pattern, Limit) ->
ets:select(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern], Limit).
-spec select_cont('$end_of_table' | tuple()) ->
'$end_of_table'
| {[{name(), type(), status()}], _Cont}.
%% @equiv ets:select(Cont)
%%
select_cont('$end_of_table') -> '$end_of_table';
select_cont(Cont) ->
ets:select(Cont).
-spec aggregate(ets:match_spec(), [datapoint()]) -> list().
%% @doc Aggregate datapoints of matching entries.
%%
%% This function selects metric entries based on the given match spec, and
%% summarizes the given datapoint values.
%%
%% Note that the match body of the match spec will be overwritten, to produce
%% only the value for each entry matching the head and guard pattern(s).
%%
%% The function can for example be used inside a function metric:
%%
%% <pre lang="erlang"><![CDATA[
%% 1> exometer:start().
%% ok
%% 2> exometer:new([g,1], gauge, []).
%% ok
%% 3> exometer:new([g,2], gauge, []).
%% ok
%% 4> exometer:new([g,3], gauge, []).
%% ok
%% 5> [exometer:update(N,V) || {N,V} <- [{[g,1],3}, {[g,2],4}, {[g,3],5}]].
%% [ok,ok,ok]
%% 6> exometer:new([g], {function,exometer,aggregate,
%% [ [{{[g,'_'],'_','_'},[],[true]}], [value] ],
%% value, [value]}, []).
%% ok
%% 7> exometer:get_value([g], [value]).
%% {ok,[{value,12}]}
%% ]]></pre>
%% @end
aggregate(Pattern, DataPoints) ->
case aggr_select(Pattern) of
[] -> [];
Found ->
aggregate(Found, DataPoints, orddict:new())
end.
aggr_select(Pattern) ->
select([setelement(3,P,[{element,1,'$_'}]) || P <- Pattern]).
aggregate([N|Ns], DPs, Acc) when is_list(N) ->
case get_value(N, DPs) of
{ok, Vals} ->
aggregate(Ns, DPs, aggr_acc(Vals, Acc));
_ ->
aggregate(Ns, DPs, Acc)
end;
aggregate([], _, Acc) ->
Acc.
aggr_acc([{D,V}|T], Acc) ->
if is_integer(V) ->
aggr_acc(T, orddict:update(D, fun(Val) ->
Val + V
end, V, Acc));
true ->
aggr_acc(T, Acc)
end;
aggr_acc([], Acc) ->
Acc.
global_status(St) when St==enabled; St==disabled ->
Prev = exometer_global:status(),
if St =:= Prev -> ok;
true ->
parse_trans_mod:transform_module(
exometer_global, fun(Forms,_) -> pt(Forms, St) end, [])
end,
Prev.
pt(Forms, St) ->
parse_trans:plain_transform(
fun(F) ->
plain_pt(F, St)
end, Forms).
plain_pt({function, L, status, 0, [_]}, St) ->
{function, L, status, 0,
[{clause, L, [], [], [{atom, L, St}]}]};
plain_pt(_, _) ->
continue.
%% Perform variable replacement in the ets select pattern.
%% We want to project the entries as a set of {Name, Type, Status} tuples.
%% This means we need to perform variable substitution in both guards and
%% match bodies. Note that the 'status' attribute is now a bit map, but we
%% want the disabled|enabled 'status' to be presented (and also to be
%% matchable).
pattern({'_', Gs, Prod}) ->
{'_', repl(Gs, g_subst(['$_'])), repl(Prod, p_subst(['$_']))};
pattern({KP, Gs, Prod}) when is_atom(KP) ->
{KP, repl(Gs, g_subst([KP,'$_'])), repl(Prod, p_subst([KP,'$_']))};
pattern({{N,T,S}, Gs, Prod}) ->
%% Remember the match head variables, at least if they are simple
%% dollar variables, so that we can perform substitution on them
%% later on.
Tail = match_tail(N,T,S),
{S1, Gs1} = repl_status(S, repl(Gs, g_subst(['$_'|Tail]))),
{#exometer_entry{name = N, type = T, status = S1, _ = '_'},
Gs1, repl(Prod, p_subst(['$_'|Tail]))}.
%% The "named variables" are for now undocumented.
repl('$name' ,_) -> {element, #exometer_entry.name, '$_'};
repl('$type' ,_) -> {element, #exometer_entry.type, '$_'};
repl('$options' ,_) -> {element, #exometer_entry.options, '$_'};
repl('$status' ,_) -> status_body_pattern();
repl('$status_bits',_) -> {element, #exometer_entry.status, '$_'};
repl('$ref' ,_) -> {element, #exometer_entry.ref, '$_'};
repl('$behaviour' ,_) -> {element, #exometer_entry.behaviour, '$_'};
repl('$cache' ,_) -> {element, #exometer_entry.cache, '$_'};
repl('$timestamp' ,_) -> {element, #exometer_entry.timestamp, '$_'};
repl(P, Subst) when is_atom(P) ->
%% Check if P is one of the match variables we've saved.
case lists:keyfind(P, 1, Subst) of
{_, Repl} -> Repl;
false -> P
end;
repl(T, Subst) when is_tuple(T) ->
list_to_tuple(repl(tuple_to_list(T), Subst));
repl([H|T], Subst) ->
[repl(H, Subst)|repl(T, Subst)];
repl(X, _) ->
X.
repl_status(S, Gs) when S==enabled; S==disabled ->
{'_', [{'=:=', status_body_pattern(), S}|Gs]};
repl_status(S, Gs) ->
{S, Gs}.
g_subst(Ks) ->
[g_subst_(K) || K <- Ks].
%% Potentially save Name, Type and Status match head patterns
match_tail(N,T,S) ->
case is_dollar(N) of true ->
[{N, {element,#exometer_entry.name,'$_'}}|match_tail(T,S)];
false -> match_tail(T, S)
end.
match_tail(T,S) ->
case is_dollar(T) of true ->
[{T, {element,#exometer_entry.type,'$_'}}|match_tail(S)];
false -> match_tail(S)
end.
match_tail(S) ->
case is_dollar(S) of true ->
[{S, status_element_pattern(S)}];
false -> []
end.
is_dollar(A) when is_atom(A) ->
case atom_to_list(A) of
[$$ | Rest] ->
try _ = list_to_integer(Rest), true
catch error:_ -> false
end;
_ -> false
end;
is_dollar(_) -> false.
g_subst_({_,_}=X) -> X;
g_subst_(K) when is_atom(K) ->
{K, {{{element,#exometer_entry.name,'$_'},
{element,#exometer_entry.type,'$_'},
status_element_pattern({element,#exometer_entry.status,'$_'})}}}.
%% The status attribute: bit 1 indicates enabled (1) or disabled (0)
status_element_pattern(S) ->
%% S is the match head pattern corresponding to the status bits
{element, {'+',{'band',S,1},1}, {{disabled,enabled}}}.
status_body_pattern() ->
{element,
{'+',{'band',
{element,#exometer_entry.status,'$_'},1},1},
{{disabled,enabled}}}.
p_subst(Ks) ->
[p_subst_(K) || K <- Ks].
p_subst_({_,_}=X) -> X;
p_subst_(K) when is_atom(K) ->
{K, {{{element,#exometer_entry.name,'$_'},
{element,#exometer_entry.type,'$_'},
status_body_pattern()}}}.
%% This function returns a list of elements for ets:update_element/3,
%% to be used for updating the #exometer_entry{} instances.
%% The options attribute is always updated, replacing old matching
%% options in the record.
process_setopts(#exometer_entry{
name = Name, ref = Ref, type = Type,
module = M, options = OldOpts} = Entry, Options) ->
case M =/= exometer andalso
erlang:function_exported(M, preprocess_setopts, 5) of
true ->
Options1 = M:preprocess_setopts(Name, Options, Type, Ref, OldOpts),
process_setopts_(Entry, Options1);
false ->
process_setopts_(Entry, Options)
end.
process_setopts_(#exometer_entry{options = OldOpts} = Entry, Options) ->
{E1, Elems} =
lists:foldr(
fun
({cache, Val},
{#exometer_entry{cache = Cache0} = E, Elems} = Acc) ->
if is_integer(Val), Val >= 0 ->
if Val =/= Cache0 ->
{E#exometer_entry{cache = Val},
add_elem(cache, Val, Elems)};
true ->
Acc
end;
true -> error({illegal, {cache, Val}})
end;
({status, Status}, {#exometer_entry{status = Status0} = E, Elems} = Acc) ->
case is_status_change(Status, Status0) of
{true, Status1} ->
{E#exometer_entry{status = Status1},
add_elem(status, Status1, Elems)};
false ->
Acc
end;
({update_event, UE},
{#exometer_entry{status = Status0} = E, Elems} = Acc)
when is_boolean(UE) ->
case (exometer_util:test_event_flag(update, Status0) == UE) of
true -> Acc;
false ->
%% value changed
if UE ->
Status = exometer_util:set_event_flag(
update, E#exometer_entry.status),
{E#exometer_entry{status = Status},
add_elem(status, Status, Elems)};
true ->
Status = exometer_util:clear_event_flag(
update, E#exometer_entry.status),
{E#exometer_entry{status = Status},
add_elem(status, Status, Elems)}
end
end;
({ref,R}, {E, Elems}) ->
{E#exometer_entry{ref = R}, add_elem(ref, R, Elems)};
({_,_}, Acc) ->
Acc
end, {Entry, []}, Options),
{E1, [{#exometer_entry.options, update_opts(Options, OldOpts)}|Elems]}.
is_status_change(enabled, St) ->
if ?IS_ENABLED(St) -> false;
true -> {true, exometer_util:set_status(enabled, St)}
end;
is_status_change(disabled, St) ->
if ?IS_DISABLED(St) -> false;
true -> {true, exometer_util:set_status(disabled, St)}
end.
add_elem(K, V, Elems) ->
P = pos(K),
lists:keystore(P, 1, Elems, {P, V}).
pos(cache) -> #exometer_entry.cache;
pos(status) -> #exometer_entry.status;
pos(ref) -> #exometer_entry.ref.
update_opts(New, Old) ->
type_arg_first(lists:foldl(
fun({K,_} = Opt, Acc) ->
lists:keystore(K, 1, Acc, Opt)
end, Old, New)).
type_arg_first([{arg,_}|_] = Opts) ->
Opts;
type_arg_first(Opts) ->
case lists:keyfind(arg, 1, Opts) of
false ->
Opts;
Arg ->
[Arg|Opts -- [Arg]]
end.
%% Retrieve individual data points for the counter maintained by
%% the exometer record itself.
get_ctr_datapoint(#exometer_entry{name = Name}, value) ->
{value, counter_sum(Name)};
get_ctr_datapoint(#exometer_entry{name = Name}, value16) ->
{value16, counter_sum(Name) rem 16#FFFF};
get_ctr_datapoint(#exometer_entry{name = Name}, value32) ->
{value32, counter_sum(Name) rem 16#FFFFFFFF};
get_ctr_datapoint(#exometer_entry{name = Name}, value64) ->
{value64, counter_sum(Name) rem 16#FFFFFFFFFFFFFFFF};
get_ctr_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) ->
{ms_since_reset, exometer_util:timestamp() - TS};
get_ctr_datapoint(#exometer_entry{}, Undefined) ->
{Undefined, undefined}.
counter_sum(Name) ->
lists:sum([ets:lookup_element(T, Name, #exometer_entry.value)
|| T <- exometer_util:tables()]).
get_gauge_datapoint(#exometer_entry{value = Value}, value) ->
{value, Value};
get_gauge_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) ->
{ms_since_reset, exometer_util:timestamp() - TS};
get_gauge_datapoint(#exometer_entry{}, Undefined) ->
{Undefined, undefined}.
get_fctr_datapoint(#exometer_entry{ref = Ref}, value) ->
case Ref of
{M, F} ->
{call_count, Res} = erlang:trace_info({M, F, 0}, call_count),
case Res of
C when is_integer(C) ->
{value, C};
_ ->
{value, 0}
end;
_ -> {value, 0}
end;
get_fctr_datapoint(#exometer_entry{timestamp = TS }, ms_since_reset) ->
{ms_since_reset, exometer_util:timestamp() - TS };
get_fctr_datapoint(#exometer_entry{ }, Undefined) ->
{Undefined, undefined}.
create_entry(#exometer_entry{module = exometer,
type = Type} = E) when Type == counter;
Type == gauge ->
E1 = E#exometer_entry{value = 0, timestamp = exometer_util:timestamp()},
insert_aliases(E1),
[ets:insert(T, E1) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
create_entry(#exometer_entry{module = exometer,
status = Status,
type = fast_counter, options = Opts} = E) ->
case lists:keyfind(function, 1, Opts) of
false ->
error({required, function});
{_, {M,F}} when is_atom(M), M =/= '_',
is_atom(F), M =/= '_' ->
code:ensure_loaded(M), % module must be loaded for trace_pattern
E1 = E#exometer_entry{ref = {M, F}, value = 0,
timestamp = exometer_util:timestamp()},
set_call_count(M, F, ?IS_ENABLED(Status)),
insert_aliases(E1),
[ets:insert(T, E1) ||
T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
Other ->
error({badarg, {function, Other}})
end;
create_entry(#exometer_entry{module = Module,
type = Type,
name = Name,
status = Status,
options = Opts} = E) ->
case
case Module:behaviour() of
probe ->
if ?IS_ENABLED(Status) ->
{probe, exometer_probe:new(
Name, Type, [{ arg, Module} | Opts ])};
?IS_DISABLED(Status) ->
%% Don't start probe if disabled
{probe, ok}
end;
entry ->
{entry, Module:new(Name, Type, Opts) };
Other -> Other
end
of
{Behaviour, ok }->
insert_aliases(E),
[ets:insert(T, E#exometer_entry { behaviour = Behaviour })
|| T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
{Behaviour, {ok, Ref}} ->
insert_aliases(E),
[ets:insert(T, E#exometer_entry{ref=Ref, behaviour=Behaviour})
|| T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
ok;
Other1 ->
Other1
end;
create_entry(_Other) ->
{error, unknown_argument}.
insert_aliases(#exometer_entry{name = Name, options = Options}) ->
case lists:keyfind(aliases, 1, Options) of
{_, Aliases} ->
case lists:all(fun({DP, Alias}) ->
(is_atom(DP) orelse is_integer(DP))
andalso (is_atom(Alias) orelse is_binary(Alias));
(_) -> false
end, Aliases) of
true ->
[exometer_alias:new(Alias, Name, DP) || {DP, Alias} <- Aliases],
ok;
false ->
erlang:error({invalid_aliases, Aliases})
end;
false ->
ok
end.
set_call_count({M, F}, Bool) ->
set_call_count(M, F, Bool).
set_call_count(M, F, Bool) when is_atom(M), is_atom(F), is_boolean(Bool) ->
erlang:trace_pattern({M, F, 0}, Bool, [call_count]).
-spec register_application() -> ok | error().
%% @equiv register_application(current_application())
%%
register_application() ->
case application:get_application() of
{ok, App} ->
register_application(App);
Other ->
{error, Other}
end.
-spec register_application(_Application::atom()) -> ok | error().
%% @doc Registers statically defined entries with exometer.
%%
%% This function can be used e.g. as a start phase hook or during upgrade.
%% It will check for the environment variables `exometer_defaults' and
%% `exometer_predefined' in `Application', and apply them as if it had
%% when exometer was first started. If the function is called again,
%% the settings are re-applied. This can be used e.g. during upgrade,
%% in order to change statically defined settings.
%%
%% If exometer is not running, the function does nothing.
%% @end
register_application(App) ->
exometer_admin:register_application(App).