Current section

Files

Jump to
prometheus src metrics prometheus_summary.erl
Raw

src/metrics/prometheus_summary.erl

-module(prometheus_summary).
-compile({parse_transform, prometheus_pt}).
-moduledoc """
Summary metric, to track the size of events.
Example use cases for Summaries: - Response latency; - Request size; - Response size.
Example:
```erlang
-module(my_proxy_instrumenter).
setup() ->
prometheus_summary:declare([{name, request_size_bytes},
{help, \"Request size in bytes.\"}]),
prometheus_summary:declare([{name, response_size_bytes},
{help, \"Response size in bytes.\"}]).
observe_request(Size) ->
prometheus_summary:observe(request_size_bytes, Size).
observe_response(Size) ->
prometheus_summary:observe(response_size_bytes, Size).
```
""".
%%% metric
-export([
new/1,
declare/1,
deregister/1,
deregister/2,
set_default/2,
observe/2,
observe/3,
observe/4,
observe_duration/2,
observe_duration/3,
observe_duration/4,
remove/1,
remove/2,
remove/3,
reset/1,
reset/2,
reset/3,
value/1,
value/2,
value/3,
values/2
]).
%%% collector
-export([
deregister_cleanup/1,
collect_mf/2,
collect_metrics/2
]).
-include("prometheus.hrl").
-behaviour(prometheus_metric).
-behaviour(prometheus_collector).
-define(TABLE, ?PROMETHEUS_SUMMARY_TABLE).
-define(ISUM_POS, 3).
-define(FSUM_POS, 4).
-define(COUNTER_POS, 2).
-define(WIDTH, 16).
-type conflict_cb() :: fun(
(
prometheus_registry:registry(),
prometheus_metric:name(),
prometheus_metric:labels(),
number()
) -> ok
).
-doc """
Creates a summary using `Spec`.
Raises:
* `{missing_metric_spec_key, Key, Spec}` error if required `Spec` key is missing.
* `{invalid_metric_name, Name, Message}` error if metric `Name` is invalid.
* `{invalid_metric_help, Help, Message}` error if metric `Help` is invalid.
* `{invalid_metric_labels, Labels, Message}` error if `Labels` isn't a list.
* `{invalid_label_name, Name, Message}` error if `Name` isn't a valid label name.
* `{invalid_value_error, Value, Message}` error if `duration_unit` is unknown or doesn't match metric name.
* `{mf_already_exists, {Registry, Name}, Message}` error if a summary with the same `Spec` already exists.
""".
-spec new(prometheus_metric:spec()) -> ok.
new(Spec) ->
validate_summary_spec(Spec),
prometheus_metric:insert_new_mf(?TABLE, ?MODULE, Spec).
-doc """
Creates a summary using `Spec`. If a summary with the same `Spec` exists returns `false`.
Raises:
* `{missing_metric_spec_key, Key, Spec}` error if required `Spec` key is missing.
* `{invalid_metric_name, Name, Message}` error if metric `Name` is invalid.
* `{invalid_metric_help, Help, Message}` error if metric `Help` is invalid.
* `{invalid_metric_labels, Labels, Message}` error if `Labels` isn't a list.
* `{invalid_label_name, Name, Message}` error if `Name` isn't a valid label name.
* `{invalid_value_error, Value, MessagE}` error if `duration_unit` is unknown or doesn't match metric name.
""".
-spec declare(prometheus_metric:spec()) -> boolean().
declare(Spec) ->
Spec1 = validate_summary_spec(Spec),
prometheus_metric:insert_mf(?TABLE, ?MODULE, Spec1).
-doc #{equiv => deregister(default, Name)}.
-spec deregister(prometheus_metric:name()) -> {boolean(), boolean()}.
deregister(Name) ->
deregister(default, Name).
-doc """
Removes all summary series with name `Name` and removes Metric Family from `Registry`.
After this call new/1 for `Name` and `Registry` will succeed.
Returns `{true, _}` if `Name` was a registered summary. Otherwise returns `{false, _}`.
""".
-spec deregister(prometheus_registry:registry(), prometheus_metric:name()) ->
{boolean(), boolean()}.
deregister(Registry, Name) ->
MFR = prometheus_metric:deregister_mf(?TABLE, Registry, Name),
NumDeleted = ets:select_delete(?TABLE, deregister_select(Registry, Name)),
{MFR, NumDeleted > 0}.
-doc false.
-spec set_default(prometheus_registry:registry(), prometheus_metric:name()) -> boolean().
set_default(Registry, Name) ->
ets:insert_new(?TABLE, {key(Registry, Name, []), 0, 0, 0}).
-doc #{equiv => observe(default, Name, [], Value)}.
-spec observe(prometheus_metric:name(), number()) -> ok.
observe(Name, Value) ->
observe(default, Name, [], Value).
-doc #{equiv => observe(default, Name, LabelValues, Value)}.
-spec observe(prometheus_metric:name(), prometheus_metric:labels(), number()) -> ok.
observe(Name, LabelValues, Value) ->
observe(default, Name, LabelValues, Value).
-doc """
Observes the given `Value`.
Raises:
* `{invalid_value, Value, Message}` if `Value` isn't an integer.
* `{unknown_metric, Registry, Name}` error if summary with named `Name` can't be found in `Registry`.
* `{invalid_metric_arity, Present, Expected}` error if labels count mismatch.
""".
-spec observe(Registry, Name, LabelValues, Value) -> ok when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:labels(),
Value :: number().
observe(Registry, Name, LabelValues, Value) when is_integer(Value) ->
Key = key(Registry, Name, LabelValues),
Spec = [{?COUNTER_POS, 1}, {?ISUM_POS, Value}],
try
ets:update_counter(?TABLE, Key, Spec)
catch
error:badarg ->
insert_metric(Registry, Name, LabelValues, Value, fun observe/4)
end,
ok;
observe(Registry, Name, LabelValues, Value) when is_number(Value) ->
Key = key(Registry, Name, LabelValues),
Spec = [{{Key, '$1', '$2', '$3'}, [], [{{{Key}, {'+', '$1', 1}, '$2', {'+', '$3', Value}}}]}],
case ets:select_replace(?TABLE, Spec) of
0 ->
insert_metric(Registry, Name, LabelValues, Value, fun observe/4);
1 ->
ok
end;
observe(_Registry, _Name, _LabelValues, Value) ->
erlang:error({invalid_value, Value, "observe accepts only numbers"}).
-doc #{equiv => observe_duration(default, Name, [], Fun)}.
-spec observe_duration(prometheus_metric:name(), fun(() -> term())) -> term().
observe_duration(Name, Fun) ->
observe_duration(default, Name, [], Fun).
-doc #{equiv => observe_duration(default, Name, LabelValues, Fun)}.
-spec observe_duration(prometheus_metric:name(), prometheus_metric:labels(), fun(() -> term())) ->
term().
observe_duration(Name, LabelValues, Fun) ->
observe_duration(default, Name, LabelValues, Fun).
-doc """
Tracks the amount of time spent executing `Fun`.
Raises:
* `{unknown_metric, Registry, Name}` error if summary with named `Name` can't be found in `Registry`.
* `{invalid_metric_arity, Present, Expected}` error if labels count mismatch.
* `{invalid_value, Value, Message}` if `Fun` isn't a function.
""".
-spec observe_duration(Registry, Name, LabelValues, Fun) -> any() when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:labels(),
Fun :: fun(() -> any()).
observe_duration(Registry, Name, LabelValues, Fun) when is_function(Fun) ->
Start = erlang:monotonic_time(),
try
Fun()
after
observe(Registry, Name, LabelValues, erlang:monotonic_time() - Start)
end;
observe_duration(_Regsitry, _Name, _LabelValues, Fun) ->
erlang:error({invalid_value, Fun, "observe_duration accepts only functions"}).
-doc #{equiv => remove(default, Name, [])}.
-spec remove(prometheus_metric:name()) -> boolean().
remove(Name) ->
remove(default, Name, []).
-doc #{equiv => remove(default, Name, LabelValues)}.
-spec remove(prometheus_metric:name(), prometheus_metric:labels()) -> boolean().
remove(Name, LabelValues) ->
remove(default, Name, LabelValues).
-doc """
Removes summary series identified by `Registry`, `Name` and `LabelValues`.
Raises:
* `{unknown_metric, Registry, Name}` error if summary with name `Name` can't be found in `Registry`.
* `{invalid_metric_arity, Present, Expected}` error if labels count mismatch.
""".
-spec remove(prometheus_registry:registry(), prometheus_metric:name(), prometheus_metric:labels()) ->
boolean().
remove(Registry, Name, LabelValues) ->
prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
case
lists:flatten([
ets:take(
?TABLE,
{Registry, Name, LabelValues, Scheduler}
)
|| Scheduler <- schedulers_seq()
])
of
[] -> false;
_ -> true
end.
-doc #{equiv => reset(default, Name, [])}.
-spec reset(prometheus_metric:name()) -> boolean().
reset(Name) ->
reset(default, Name, []).
-doc #{equiv => reset(default, Name, LabelValues)}.
-spec reset(prometheus_metric:name(), prometheus_metric:labels()) -> boolean().
reset(Name, LabelValues) ->
reset(default, Name, LabelValues).
-doc """
Resets the value of the summary identified by `Registry`, `Name` and `LabelValues`.
Raises:
* `{unknown_metric, Registry, Name}` error if summary with name `Name` can't be found in `Registry`.
* `{invalid_metric_arity, Present, Expected}` error if labels count mismatch.
""".
-spec reset(prometheus_registry:registry(), prometheus_metric:name(), prometheus_metric:labels()) ->
boolean().
reset(Registry, Name, LabelValues) ->
prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
case
lists:usort([
ets:update_element(
?TABLE,
{Registry, Name, LabelValues, Scheduler},
[{?COUNTER_POS, 0}, {?ISUM_POS, 0}, {?FSUM_POS, 0}]
)
|| Scheduler <- schedulers_seq()
])
of
[_, _] -> true;
[true] -> true;
_ -> false
end.
-doc #{equiv => value(default, Name, [])}.
-spec value(prometheus_metric:name()) -> {integer(), number()} | undefined.
value(Name) ->
value(default, Name, []).
-doc #{equiv => value(default, Name, LabelValues)}.
-spec value(prometheus_metric:name(), prometheus_metric:labels()) ->
{integer(), number()} | undefined.
value(Name, LabelValues) ->
value(default, Name, LabelValues).
-doc """
Returns the value of the summary identified by `Registry`, `Name` and `LabelValues`.
If there is no summary for `LabelValues`, returns `undefined`.
If duration unit set, sum will be converted to the duration unit.
[Read more here.](`m:prometheus_time`)
Raises:
* `{unknown_metric, Registry, Name}` error if summary named `Name` can't be found in `Registry`.
* `{invalid_metric_arity, Present, Expected}` error if labels count mismatch.
""".
-spec value(prometheus_registry:registry(), prometheus_metric:name(), prometheus_metric:labels()) ->
{integer(), number()} | undefined.
value(Registry, Name, LabelValues) ->
MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
DU = prometheus_metric:mf_duration_unit(MF),
case
ets:select(?TABLE, [{{{Registry, Name, LabelValues, '_'}, '$1', '$2', '$3'}, [], ['$$']}])
of
[] ->
undefined;
Values ->
{Count, Sum} = reduce_values(Values),
{Count, prometheus_time:maybe_convert_to_du(DU, Sum)}
end.
-spec values(prometheus_registry:registry(), prometheus_metric:name()) ->
[prometheus_model:'Summary'()].
values(Registry, Name) ->
case prometheus_metric:check_mf_exists(?TABLE, Registry, Name) of
false ->
[];
MF ->
DU = prometheus_metric:mf_duration_unit(MF),
Labels = prometheus_metric:mf_labels(MF),
MFValues = load_all_values(Registry, Name),
Foldl = fun([L, C, IS, FS], ResAcc) ->
{PrevCount, PrevSum} = maps:get(L, ResAcc, {0, 0}),
ResAcc#{L => {PrevCount + C, PrevSum + IS + FS}}
end,
ReducedMap = lists:foldl(Foldl, #{}, MFValues),
ReducedMapList = lists:sort(maps:to_list(ReducedMap)),
Foldr = fun({LabelValues, {Count, Sum}}, Acc) ->
[
{
lists:zip(Labels, LabelValues),
Count,
prometheus_time:maybe_convert_to_du(DU, Sum)
}
| Acc
]
end,
lists:foldr(Foldr, [], ReducedMapList)
end.
%%====================================================================
%% Collector API
%%====================================================================
-doc false.
-spec deregister_cleanup(prometheus_registry:registry()) -> ok.
deregister_cleanup(Registry) ->
prometheus_metric:deregister_mf(?TABLE, Registry),
true = ets:match_delete(?TABLE, {{Registry, '_', '_', '_'}, '_', '_', '_'}),
ok.
-doc false.
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
collect_mf(Registry, Callback) ->
[
Callback(create_summary(Name, Help, {CLabels, Labels, Registry, DU}))
|| [Name, {Labels, Help}, CLabels, DU, _] <- prometheus_metric:metrics(?TABLE, Registry)
],
ok.
-doc false.
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
[prometheus_model:'Metric'()].
collect_metrics(Name, {CLabels, Labels, Registry, DU}) ->
MFValues = load_all_values(Registry, Name),
ReducedMap = lists:foldl(
fun([L, C, IS, FS], ResAcc) ->
{PrevCount, PrevSum} = maps:get(L, ResAcc, {0, 0}),
ResAcc#{L => {PrevCount + C, PrevSum + IS + FS}}
end,
#{},
MFValues
),
ReducedMapList = lists:sort(maps:to_list(ReducedMap)),
lists:foldr(
fun({LabelValues, {Count, Sum}}, Acc) ->
[
prometheus_model_helpers:summary_metric(
CLabels ++ lists:zip(Labels, LabelValues),
Count,
prometheus_time:maybe_convert_to_du(DU, Sum)
)
| Acc
]
end,
[],
ReducedMapList
).
%%====================================================================
%% Private Parts
%%====================================================================
deregister_select(Registry, Name) ->
[{{{Registry, Name, '_', '_'}, '_', '_', '_'}, [], [true]}].
validate_summary_spec(Spec) ->
Labels = prometheus_metric_spec:labels(Spec),
validate_summary_labels(Labels),
Spec.
validate_summary_labels(Labels) ->
[raise_error_if_quantile_label_found(Label) || Label <- Labels].
raise_error_if_quantile_label_found("quantile") ->
erlang:error(
{invalid_metric_label_name, "quantile", "summary cannot have a label named \"quantile\""}
);
raise_error_if_quantile_label_found(Label) ->
Label.
-spec insert_metric(Registry, Name, LabelValues, Value, ConflictCB) -> ok when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:labels(),
Value :: number(),
ConflictCB :: conflict_cb().
insert_metric(Registry, Name, LabelValues, Value, ConflictCB) ->
prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
case ets:insert_new(?TABLE, {key(Registry, Name, LabelValues), 1, 0, Value}) of
%% some sneaky process already inserted
false ->
ConflictCB(Registry, Name, LabelValues, Value);
true ->
ok
end.
load_all_values(Registry, Name) ->
ets:match(?TABLE, {{Registry, Name, '$1', '_'}, '$2', '$3', '$4'}).
schedulers_seq() ->
lists:seq(0, ?WIDTH - 1).
key(Registry, Name, LabelValues) ->
X = erlang:system_info(scheduler_id),
Rnd = X band (?WIDTH - 1),
{Registry, Name, LabelValues, Rnd}.
-spec reduce_values([[number()]]) -> {number(), number()}.
reduce_values(Values) ->
{lists:sum([C || [C, _, _] <- Values]), lists:sum([IS + FS || [_, IS, FS] <- Values])}.
create_summary(Name, Help, Data) ->
prometheus_model_helpers:create_mf(Name, Help, summary, ?MODULE, Data).