Packages
prometheus
6.0.1
6.1.3
6.1.2
6.1.1
6.1.0
6.0.3
6.0.2
6.0.1
6.0.0
5.1.1
5.1.0
5.0.0
4.13.0
retired
4.12.0
4.11.0
4.10.0
4.9.1
4.9.0
4.8.2
4.8.1
4.8.0
4.6.0
4.5.0
4.4.1
4.4.0
4.3.0
4.2.2
4.2.0
4.1.0
4.0.1
4.0.0
3.5.1
3.5.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.2
3.3.1
3.3.0
3.2.3
3.2.2
3.2.1
3.1.1
3.1.0
3.0.1
3.0.0
3.0.0-rc1
3.0.0-alpha9
3.0.0-alpha8
3.0.0-alpha7
3.0.0-alpha6
3.0.0-alpha5
3.0.0-alpha4
3.0.0-alpha3
3.0.0-alpha2
3.0.0-alpha10
3.0.0-alpha1
2.2.0
2.1.0
2.0.0
1.7.0
1.6.0
1.5.0
1.0.2
1.0.1
1.0.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Prometheus.io client in Erlang
Current section
Files
Jump to
Current section
Files
src/metrics/prometheus_quantile_summary.erl
-module(prometheus_quantile_summary).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC("""
Summary metric, to track the size of events and report quantiles Based on prometheus_summary
Example use cases for Summaries:
* Response latency;
* Request size;
* Response size.
Example:
```erlang
-module(my_proxy_instrumenter).
setup() ->
prometheus_quantile_summary:declare(#{name => request_size_bytes,
help => \"Request size in bytes.\"}),
prometheus_quantile_summary:declare(#{name => response_size_bytes,
help => \"Response size in bytes.\"}).
observe_request(Size) ->
prometheus_quantile_summary:observe(request_size_bytes, Size).
observe_response(Size) ->
prometheus_quantile_summary:observe(response_size_bytes, Size).
```
Reports:
```text
request_size_bytes_size
request_size_bytes_count
request_size_bytes\{quantile=\"0.5\"\}
request_size_bytes\{quantile=\"0.9\"\}
request_size_bytes\{quantile=\"0.95\"\}
```
### Configuration
It takes `error` and `bound` as in `t:ddskerl_ets:opts/0`.
""").
-define(WIDTH, 16).
%%% metric
-export([
new/1,
declare/1,
set_default/2,
deregister/1,
deregister/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_QUANTILE_SUMMARY_TABLE).
?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) ->
Spec1 = validate_summary_spec(Spec),
prometheus_metric:insert_new_mf(?TABLE, ?MODULE, Spec1).
?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(false).
-spec set_default(prometheus_registry:registry(), prometheus_metric:name()) -> boolean().
set_default(Registry, Name) ->
#{error := Error, bound := Bound} = get_configuration(Registry, Name),
Key = key(Registry, Name, []),
ddskerl_ets:new(?TABLE, Key, Error, Bound).
?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(#{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:label_values(), 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:label_values(),
Value :: number().
observe(Registry, Name, LabelValues, Value) when is_number(Value) ->
Key = key(Registry, Name, LabelValues),
case ets:member(?TABLE, Key) of
true ->
ddskerl_ets:insert(?TABLE, Key, Value);
false ->
insert_metric(Registry, Name, LabelValues, Key),
observe(Registry, Name, LabelValues, Value)
end,
ok;
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(() -> dynamic())) -> dynamic().
observe_duration(Name, Fun) ->
observe_duration(default, Name, [], Fun).
?DOC(#{equiv => observe_duration(default, Name, LabelValues, Fun)}).
-spec observe_duration(Name, LabelValues, Value) -> dynamic() when
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:label_values(),
Value :: fun(() -> dynamic()).
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, Value) -> dynamic() when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:label_values(),
Value :: fun(() -> dynamic()).
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:label_values()) -> 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(Registry, Name, LabelValues) -> boolean() when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:label_values().
remove(Registry, Name, LabelValues) ->
prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
List = lists:flatten([
ets:take(?TABLE, {Registry, Name, LabelValues, SId})
|| SId <- schedulers_seq()
]),
case List 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:label_values()) -> 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(Registry, Name, LabelValues) -> boolean() when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:label_values().
reset(Registry, Name, LabelValues) ->
_ = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
[
catch ddskerl_ets:reset(?TABLE, {Registry, Name, LabelValues, SId})
|| SId <- schedulers_seq()
],
true.
?DOC(#{equiv => value(default, Name, [])}).
-spec value(prometheus_metric:name()) ->
{non_neg_integer(), number(), [{float(), float()}]} | undefined.
value(Name) ->
value(default, Name, []).
?DOC(#{equiv => value(default, Name, LabelValues)}).
-spec value(prometheus_metric:name(), prometheus_metric:label_values()) ->
{non_neg_integer(), number(), [{float(), float()}]} | 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(Registry, Name, LabelValues) -> Result when
Registry :: prometheus_registry:registry(),
Name :: prometheus_metric:name(),
LabelValues :: prometheus_metric:label_values(),
Result :: {non_neg_integer(), number(), [{float(), float()}]} | undefined.
value(Registry, Name, LabelValues) ->
MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
case
lists:any(
fun(SId) -> ets:member(?TABLE, {Registry, Name, LabelValues, SId}) end, schedulers_seq()
)
of
false ->
undefined;
true ->
DU = prometheus_metric:mf_duration_unit(MF),
#{quantiles := QNs} = prometheus_metric:mf_data(MF),
[First | Rest] = lists:flatmap(
fun(SId) ->
ets:lookup(?TABLE, {Registry, Name, LabelValues, SId})
end,
schedulers_seq()
),
Total = lists:foldl(
fun(Elem, Acc) -> ddskerl_ets:merge_tuples(Acc, Elem) end, First, Rest
),
case ddskerl_ets:total_tuple(Total) of
0 ->
{0, 0, []};
Count ->
Sum = ddskerl_ets:sum_tuple(Total),
DuSum = prometheus_time:maybe_convert_to_du(DU, Sum),
Values = [
{QN,
prometheus_time:maybe_convert_to_du(
DU,
ddskerl_ets:quantile_tuple(Total, QN)
)}
|| QN <- QNs
],
{Count, DuSum, Values}
end
end.
-spec default_quantiles() -> [float()].
default_quantiles() ->
[0.5, 0.90, 0.95].
-spec values(prometheus_registry:registry(), prometheus_metric:name()) ->
[{[{atom(), dynamic()}], non_neg_integer(), infinity | number(), [{float(), float()}]}].
values(Registry, Name) ->
case prometheus_metric:check_mf_exists(?TABLE, Registry, Name) of
false ->
[];
MF ->
Labels = prometheus_metric:mf_labels(MF),
CLabels = prometheus_metric:mf_constant_labels(MF),
Fun = fun value_summary_metric/6,
loop_through_keys(Name, Fun, CLabels, Labels, Registry)
end.
%%====================================================================
%% Collector API
%%====================================================================
?DOC(false).
-spec deregister_cleanup(prometheus_registry:registry()) -> ok.
deregister_cleanup(Registry) ->
prometheus_metric:deregister_mf(?TABLE, Registry),
ets:select_delete(?TABLE, clean_registry_select(Registry)),
ok.
?DOC(false).
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
collect_mf(Registry, Callback) ->
Metrics = prometheus_metric:metrics(?TABLE, Registry),
[
Callback(create_summary(Name, Help, {CLabels, Labels, Registry, DU, Data}))
|| [Name, {Labels, Help}, CLabels, DU, Data] <- Metrics
],
ok.
?DOC(false).
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
[prometheus_model:'Metric'()].
collect_metrics(Name, {CLabels, Labels, Registry, _DU, _Configuration}) ->
Fun = fun model_summary_metric/6,
loop_through_keys(Name, Fun, CLabels, Labels, Registry).
loop_through_keys(Name, Fun, CLabels, Labels, Registry) ->
Sets = sets:new([{version, 2}]),
First = ets:first(?TABLE),
loop_through_keys(Name, Fun, CLabels, Labels, Registry, Sets, [], First).
loop_through_keys(_, _, _, _, _, _, Acc, '$end_of_table') ->
Acc;
loop_through_keys(
Name, Fun, CLabels, Labels, Registry, Set, Acc, {Registry, Name, LabelValues, _} = CurrentKey
) ->
Key = {Registry, Name, LabelValues},
case sets:is_element(Key, Set) of
true ->
NextKey = ets:next(?TABLE, CurrentKey),
loop_through_keys(Name, Fun, CLabels, Labels, Registry, Set, Acc, NextKey);
false ->
{Count, Sum, QNs} = value(Registry, Name, LabelValues),
Value = Fun(CLabels, Labels, LabelValues, Count, Sum, QNs),
NewAcc = [Value | Acc],
NewSet = sets:add_element(Key, Set),
NextKey = ets:next(?TABLE, CurrentKey),
loop_through_keys(Name, Fun, CLabels, Labels, Registry, NewSet, NewAcc, NextKey)
end;
loop_through_keys(Name, Fun, CLabels, Labels, Registry, Set, Acc, CurrentKey) ->
NextKey = ets:next(?TABLE, CurrentKey),
loop_through_keys(Name, Fun, CLabels, Labels, Registry, Set, Acc, NextKey).
model_summary_metric(CLabels, Labels, LabelValues, Count, Sum, QNs) ->
Labs = CLabels ++ lists:zip(Labels, LabelValues),
prometheus_model_helpers:summary_metric(Labs, Count, Sum, QNs).
value_summary_metric(_CLabels, Labels, LabelValues, Count, Sum, QNs) ->
{lists:zip(Labels, LabelValues), Count, Sum, QNs}.
%%====================================================================
%% Private Parts
%%====================================================================
clean_registry_select(Registry) ->
[
{'$1',
[
{'is_tuple', {element, 1, '$1'}},
{'==', Registry, {element, 1, {element, 1, '$1'}}}
],
[true]}
].
deregister_select(Registry, Name) ->
[
{'$1',
[
{'is_tuple', {element, 1, '$1'}},
{'==', Registry, {element, 1, {element, 1, '$1'}}},
{'==', Name, {element, 2, {element, 1, '$1'}}}
],
[true]}
].
validate_summary_spec(Spec) ->
QNs = prometheus_metric_spec:get_value(quantiles, Spec, default_quantiles()),
Error = prometheus_metric_spec:get_value(error, Spec, 0.01),
Bound = prometheus_metric_spec:get_value(bound, Spec, 2184),
validate_error(Error),
validate_bound(Bound),
Data = #{
ets_table => ?TABLE,
quantiles => QNs,
error => Error,
bound => Bound
},
prometheus_metric_spec:add_value(data, Data, Spec).
insert_metric(Registry, Name, LabelValues, Key) ->
MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues),
Configuration0 = prometheus_metric:mf_data(MF),
Configuration = Configuration0#{name => Key},
ddskerl_ets:new(Configuration).
get_configuration(Registry, Name) ->
MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name),
prometheus_metric:mf_data(MF).
key(Registry, Name, LabelValues) ->
X = erlang:system_info(scheduler_id),
Rnd = X band (?WIDTH - 1),
{Registry, Name, LabelValues, Rnd}.
schedulers_seq() ->
lists:seq(0, ?WIDTH - 1).
create_summary(Name, Help, Data) ->
prometheus_model_helpers:create_mf(Name, Help, summary, ?MODULE, Data).
validate_error(Error) when is_number(Error), 0.0 < Error, Error < 100.0 ->
ok;
validate_error(Error) ->
erlang:error({invalid_error, Error, "Error should be a percentage point in (0,100)"}).
validate_bound(Bound) when is_integer(Bound), 0 < Bound ->
ok;
validate_bound(Bound) ->
erlang:error({invalid_bound, Bound, "Bound should be a positive integer"}).