-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"}).