%% @doc %% A Histogram tracks the size and number of events in buckets. %% You can use Histograms for aggregatable calculation of quantiles. %% %% Example use cases for Histograms: %% %% %% Histogram expects `buckets' key in a metric spec. Buckets can be: %% - a list of numbers in increasing order; %% - :default; %% - {:linear, start, step, count}; %% - {:exponential, start, step, count} %% %% Example: %%
%% -module(example_instrumenter).
%% setup() ->
%%   prometheus_histogram:declare([{name, http_request_duration_milliseconds},
%%                                 {labels, [method]},
%%                                 {buckets, [100, 300, 500, 750, 1000]},
%%                                 {help, "Http Request execution time."}]).
%%
%% instrument(Time, Method) ->
%%   %% Time must be in native units, otherwise duration_unit must be false
%%   prometheus_histogram:observe(http_request_duration_milliseconds,
%%                                [Method], Time).
%%
%% 
%% @end -module(prometheus_histogram). %%% metric -export([new/1, declare/1, deregister/1, deregister/2, set_default/2, observe/2, observe/3, observe/4, pobserve/6, 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, buckets/1, buckets/2, buckets/3, values/2] ). %%% collector -export([deregister_cleanup/1, collect_mf/2, collect_metrics/2]). -include("prometheus.hrl"). -behaviour(prometheus_metric). -behaviour(prometheus_collector). %%==================================================================== %% Macros %%==================================================================== -define(TABLE, ?PROMETHEUS_HISTOGRAM_TABLE). -define(BOUNDS_POS, 2). -define(ISUM_POS, 3). -define(FSUM_POS, 4). -define(BUCKETS_START, 5). -define(WIDTH, 16). %% ets row layout %% {Key, NBounds, Sum, Bucket1, Bucket2, ...} %% NBounds is a list of bounds possibly converted to native units %%==================================================================== %% Metric API %%==================================================================== %% @doc Creates a histogram using `Spec'. %% %% Raises `{missing_metric_spec_key, Key, Spec}' error if required `Soec' key %% is missing.
%% Raises `{invalid_metric_name, Name, Message}' error if metric `Name' %% is invalid.
%% Raises `{invalid_metric_help, Help, Message}' error if metric `Help' %% is invalid.
%% Raises `{invalid_metric_labels, Labels, Message}' error if `Labels' %% isn't a list.
%% Raises `{invalid_label_name, Name, Message}' error if `Name' isn't a valid %% label name.
%% Raises `{invalid_value_error, Value, Message}' error if `duration_unit' is %% unknown or doesn't match metric name.
%% Raises `{mf_already_exists, {Registry, Name}, Message}' error if a histogram %% with the same `Spec' already exists. %% %% Histogram-specific errors:
%% Raises `{no_buckets, Buckets}' error if `Buckets' are missing, %% not a list, empty list or not known buckets spec.
%% Raises `{invalid_buckets, Buckets, Message}' error if `Buckets' %% aren't in increasing order.
%% Raises `{invalid_bound, Bound}' error if `Bound' isn't a number. %% @end new(Spec) -> Spec1 = validate_histogram_spec(Spec), prometheus_metric:insert_new_mf(?TABLE, ?MODULE, Spec1). %% @doc Creates a histogram using `Spec'. %% If a histogram with the same `Spec' exists returns `false'. %% %% Raises `{missing_metric_spec_key, Key, Spec}' error if required `Soec' key %% is missing.
%% Raises `{invalid_metric_name, Name, Message}' error if metric `Name' %% is invalid.
%% Raises `{invalid_metric_help, Help, Message}' error if metric `Help' %% is invalid.
%% Raises `{invalid_metric_labels, Labels, Message}' error if `Labels' %% isn't a list.
%% Raises `{invalid_label_name, Name, Message}' error if `Name' isn't a valid %% label name.
%% Raises `{invalid_value_error, Value, MessagE}' error if `duration_unit' is %% unknown or doesn't match metric name.
%% %% Histogram-specific errors:
%% Raises `{no_buckets, Buckets}' error if `Buckets' are missing, %% not a list, empty list or not known buckets spec.
%% Raises `{invalid_buckets, Buckets, Message}' error if `Buckets' %% aren't in increasing order.
%% Raises `{invalid_bound, Bound}' error if `Bound' isn't a number. %% @end declare(Spec) -> Spec1 = validate_histogram_spec(Spec), prometheus_metric:insert_mf(?TABLE, ?MODULE, Spec1). %% @equiv deregister(default, Name) deregister(Name) -> deregister(default, Name). %% @doc %% Removes all histogram 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 histogram. %% Otherwise returns `{false, _}'. %% @end deregister(Registry, Name) -> try MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name), Buckets = prometheus_metric:mf_data(MF), prometheus_metric:deregister_mf(?TABLE, Registry, Name), Select = deregister_select(Registry, Name, Buckets), NumDeleted = ets:select_delete(?TABLE, Select), {true, NumDeleted > 0} catch _:_ -> {false, false} end. %% @private set_default(Registry, Name) -> insert_placeholders(Registry, Name, []). %% @equiv observe(default, Name, [], Value) observe(Name, Value) -> observe(default, Name, [], Value). %% @equiv observe(default, Name, LabelValues, Value) 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.
%% Raises `{unknown_metric, Registry, Name}' error if histogram with named %% `Name' can't be found in `Registry'.
%% Raises `{invalid_metric_arity, Present, Expected}' error if labels count %% mismatch. %% @end observe(Registry, Name, LabelValues, Value) when is_integer(Value) -> Key = key(Registry, Name, LabelValues), case ets:lookup(?TABLE, Key) of [Metric] -> BucketPosition = calculate_histogram_bucket_position(Metric, Value), ets:update_counter(?TABLE, Key, [{?ISUM_POS, Value}, {?BUCKETS_START + BucketPosition, 1}]); [] -> 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), case ets:lookup(?TABLE, Key) of [Metric] -> fobserve_impl(Key, Metric, Value); [] -> insert_metric(Registry, Name, LabelValues, Value, fun(_, _, _, _) -> observe(Registry, Name, LabelValues, Value) end) end; observe(_Registry, _Name, _LabelValues, Value) -> erlang:error({invalid_value, Value, "observe accepts only numbers"}). %% @private pobserve(Registry, Name, LabelValues, Buckets, BucketPos, Value) when is_integer(Value) -> Key = key(Registry, Name, LabelValues), try ets:update_counter(?TABLE, Key, [{?ISUM_POS, Value}, {?BUCKETS_START + BucketPos, 1}]) catch error:badarg -> insert_metric(Registry, Name, LabelValues, Value, fun(_, _, _, _) -> pobserve(Registry, Name, LabelValues, Buckets, BucketPos, Value) end) end, ok; pobserve(Registry, Name, LabelValues, Buckets, BucketPos, Value) when is_number(Value) -> Key = key(Registry, Name, LabelValues), case fobserve_impl(Key, Buckets, BucketPos, Value) of 0 -> insert_metric(Registry, Name, LabelValues, Value, fun(_, _, _, _) -> fobserve_impl(Key, Buckets, BucketPos, Value) end); 1 -> ok end; pobserve(_Registry, _Name, _LabelValues, _Buckets, _Pos, Value) -> erlang:error({invalid_value, Value, "pobserve accepts only numbers"}). %% @equiv observe_duration(default, Name, [], Fun) observe_duration(Name, Fun) -> observe_duration(default, Name, [], Fun). %% @equiv observe_duration(default, Name, LabelValues, Fun) 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 histogram with named %% `Name' can't be found in `Registry'.
%% Raises `{invalid_metric_arity, Present, Expected}' error if labels count %% mismatch. %% Raises `{invalid_value, Value, Message}' if `Fun' %% isn't a function.
%% @end 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"}). %% @equiv remove(default, Name, []) remove(Name) -> remove(default, Name, []). %% @equiv remove(default, Name, LabelValues) remove(Name, LabelValues) -> remove(default, Name, LabelValues). %% @doc Removes histogram series identified by `Registry', `Name' %% and `LabelValues'. %% %% Raises `{unknown_metric, Registry, Name}' error if histogram with name %% `Name' can't be found in `Registry'.
%% Raises `{invalid_metric_arity, Present, Expected}' error if labels count %% mismatch. %% @end 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. %% @equiv reset(default, Name, []) reset(Name) -> reset(default, Name, []). %% @equiv reset(default, Name, LabelValues) reset(Name, LabelValues) -> reset(default, Name, LabelValues). %% @doc Resets the value of the histogram identified by `Registry', `Name' %% and `LabelValues'. %% %% Raises `{unknown_metric, Registry, Name}' error if histogram with name %% `Name' can't be found in `Registry'.
%% Raises `{invalid_metric_arity, Present, Expected}' error if labels count %% mismatch. %% @end reset(Registry, Name, LabelValues) -> MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues), Buckets = prometheus_metric:mf_data(MF), UpdateSpec = generate_update_spec(Buckets), case lists:usort([ets:update_element(?TABLE, {Registry, Name, LabelValues, Scheduler}, [{?ISUM_POS, 0}, {?FSUM_POS, 0}] ++ UpdateSpec) || Scheduler <- schedulers_seq()]) of [_, _] -> true; [true] -> true; _ -> false end. %% @equiv value(default, Name, []) value(Name) -> value(default, Name, []). %% @equiv value(default, Name, LabelValues) value(Name, LabelValues) -> value(default, Name, LabelValues). %% @doc Returns the value of the histogram identified by `Registry', `Name' %% and `LabelValues'. If there is no histogram for `LabelValues', %% returns `undefined'. %% %% If duration unit set, sum will be converted to the duration unit. %% {@link prometheus_time. Read more here.} %% %% Raises `{unknown_metric, Registry, Name}' error if histogram named `Name' %% can't be found in `Registry'.
%% Raises `{invalid_metric_arity, Present, Expected}' error if labels count %% mismatch. %% @end value(Registry, Name, LabelValues) -> MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues), RawValues = [ets:lookup(?TABLE, {Registry, Name, LabelValues, Scheduler}) || Scheduler <- schedulers_seq()], case lists:flatten(RawValues) of [] -> undefined; Values -> {reduce_buckets_counters(Values), reduce_sum(MF, Values)} end. values(Registry, Name) -> case prometheus_metric:check_mf_exists(?TABLE, Registry, Name) of false -> []; MF -> mf_values(Registry, Name, MF) end. %% @equiv buckets(default, Name, []) buckets(Name) -> buckets(default, Name, []). %% @equiv buckets(default, Name, LabelValues) buckets(Name, LabelValues) -> buckets(default, Name, LabelValues). %% @doc Returns buckets of the histogram identified by `Registry', `Name' %% and `LabelValues'. %% @end buckets(Registry, Name, LabelValues) -> MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues), prometheus_metric:mf_data(MF). %%==================================================================== %% Collector API %%==================================================================== %% @private deregister_cleanup(Registry) -> [delete_metrics(Registry, Buckets) || [_, _, _, _, Buckets] <- prometheus_metric:metrics(?TABLE, Registry)], true = prometheus_metric:deregister_mf(?TABLE, Registry), ok. %% @private collect_mf(Registry, Callback) -> [Callback(create_histogram(Name, Help, {CLabels, Labels, Registry, DU, Buckets})) || [Name, {Labels, Help}, CLabels, DU, Buckets] <- prometheus_metric:metrics(?TABLE, Registry)], ok. %% @private collect_metrics(Name, {CLabels, Labels, Registry, DU, Bounds}) -> MFValues = load_all_values(Registry, Name, Bounds), LabelValuesMap = reduce_label_values(MFValues), maps:fold( fun(LabelValues, Stat, L) -> [create_histogram_metric(CLabels, Labels, DU, Bounds, LabelValues, Stat)|L] end, [], LabelValuesMap). %%==================================================================== %% Private Parts %%==================================================================== validate_histogram_spec(Spec) -> Labels = prometheus_metric_spec:labels(Spec), validate_histogram_labels(Labels), RBuckets = prometheus_metric_spec:get_value(buckets, Spec, default), Buckets = prometheus_buckets:new(RBuckets), [{data, Buckets}|Spec]. validate_histogram_labels(Labels) -> [raise_error_if_le_label_found(Label) || Label <- Labels]. raise_error_if_le_label_found("le") -> erlang:error({invalid_metric_label_name, "le", "histogram cannot have a label named \"le\""}); raise_error_if_le_label_found(Label) -> Label. insert_metric(Registry, Name, LabelValues, Value, CB) -> insert_placeholders(Registry, Name, LabelValues), CB(Registry, Name, LabelValues, Value). fobserve_impl(Key, Metric, Value) -> Buckets = metric_buckets(Metric), BucketPos = calculate_histogram_bucket_position(Metric, Value), fobserve_impl(Key, Buckets, BucketPos, Value). fobserve_impl(Key, Buckets, BucketPos, Value) -> ets:select_replace(?TABLE, generate_select_replace(Key, Buckets, BucketPos, Value)). insert_placeholders(Registry, Name, LabelValues) -> MF = prometheus_metric:check_mf_exists(?TABLE, Registry, Name, LabelValues), MFBuckets = prometheus_metric:mf_data(MF), DU = prometheus_metric:mf_duration_unit(MF), Fun = fun (Bucket) -> prometheus_time:maybe_convert_to_native(DU, Bucket) end, BoundCounters = lists:duplicate(length(MFBuckets), 0), MetricSpec = [key(Registry, Name, LabelValues), lists:map(Fun, MFBuckets), 0, 0] ++ BoundCounters, ets:insert_new(?TABLE, list_to_tuple(MetricSpec)). calculate_histogram_bucket_position(Metric, Value) -> Buckets = metric_buckets(Metric), prometheus_buckets:position(Buckets, Value). generate_select_replace(Key, Bounds, BucketPos, Value) -> BoundPlaceholders = gen_query_bound_placeholders(Bounds), HistMatch = list_to_tuple([Key, '$2', '$3', '$4'] ++ BoundPlaceholders), BucketUpdate = lists:sublist(BoundPlaceholders, BucketPos) ++ [{'+', gen_query_placeholder(?BUCKETS_START + BucketPos), 1}] ++ lists:nthtail(BucketPos + 1, BoundPlaceholders), HistUpdate = list_to_tuple([{Key}, '$2', '$3', {'+', '$4', Value}] ++ BucketUpdate), [{HistMatch, [], [{HistUpdate}]}]. buckets_seq(Buckets) -> lists:seq(?BUCKETS_START, ?BUCKETS_START + length(Buckets) - 1). generate_update_spec(Buckets) -> [{Index, 0} || Index <- buckets_seq(Buckets)]. gen_query_placeholder(Index) -> list_to_atom("$" ++ integer_to_list(Index)). gen_query_bound_placeholders(Buckets) -> [gen_query_placeholder(Index) || Index <- buckets_seq(Buckets)]. augment_counters([Start | Counters]) -> augment_counters(Counters, [Start], Start). augment_counters([], LAcc, _CAcc) -> LAcc; augment_counters([Counter | Counters], LAcc, CAcc) -> augment_counters(Counters, LAcc ++ [CAcc + Counter], CAcc + Counter). metric_buckets(Metric) -> element(?BOUNDS_POS, Metric). reduce_buckets_counters(Metrics) -> ABuckets = [sub_tuple_to_list(Metric, ?BUCKETS_START, ?BUCKETS_START + length(metric_buckets(Metric))) || Metric <- Metrics], [lists:sum(Bucket) || Bucket <- transpose(ABuckets)]. transpose([[]|_]) -> []; transpose(M) -> [lists:map(fun hd/1, M) | transpose(lists:map(fun tl/1, M))]. reduce_sum(Metrics) -> lists:sum([element(?ISUM_POS, Metric) + element(?FSUM_POS, Metric) || Metric <- Metrics]). reduce_sum(MF, Metrics) -> DU = prometheus_metric:mf_duration_unit(MF), prometheus_time:maybe_convert_to_du(DU, reduce_sum(Metrics)). create_histogram_metric(CLabels, Labels, DU, Bounds, LabelValues, [ISum, FSum | Buckets]) -> BCounters = augment_counters(Buckets), Bounds1 = lists:zipwith(fun(Bound, Bucket) -> {Bound, Bucket} end, Bounds, BCounters), prometheus_model_helpers:histogram_metric( CLabels ++ lists:zip(Labels, LabelValues), Bounds1, lists:last(BCounters), prometheus_time:maybe_convert_to_du(DU, ISum + FSum)). load_all_values(Registry, Name, Bounds) -> BoundPlaceholders = gen_query_bound_placeholders(Bounds), QuerySpec = [{Registry, Name, '$1', '_'}, '_', '$3', '$4'] ++ BoundPlaceholders, ets:match(?TABLE, list_to_tuple(QuerySpec)). deregister_select(Registry, Name, Buckets) -> BoundCounters = lists:duplicate(length(Buckets), '_'), MetricSpec = [{Registry, Name, '_', '_'}, '_', '_', '_'] ++ BoundCounters, [{list_to_tuple(MetricSpec), [], [true]}]. delete_metrics(Registry, Buckets) -> BoundCounters = lists:duplicate(length(Buckets), '_'), MetricSpec = [{Registry, '_', '_', '_'}, '_', '_', '_'] ++ BoundCounters, ets:match_delete(?TABLE, list_to_tuple(MetricSpec)). sub_tuple_to_list(Tuple, Pos, Size) when Pos < Size -> [element(Pos, Tuple) | sub_tuple_to_list(Tuple, Pos + 1, Size)]; sub_tuple_to_list(_Tuple, _Pos, _Size) -> []. 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}. reduce_label_values(MFValues) -> lists:foldl( fun([Labels | V], ResAcc) when is_map_key(Labels, ResAcc) -> PrevSum = maps:get(Labels, ResAcc), ResAcc#{Labels => [lists:sum(C) || C <- transpose([PrevSum, V])]}; ([Labels | V], ResAcc) -> ResAcc#{Labels => V} end, #{}, MFValues). mf_values(Registry, Name, MF) -> DU = prometheus_metric:mf_duration_unit(MF), Labels = prometheus_metric:mf_labels(MF), Bounds = prometheus_metric:mf_data(MF), MFValues = load_all_values(Registry, Name, Bounds), LabelValuesMap = reduce_label_values(MFValues), maps:fold( fun(LabelValues, [ISum, FSum | BCounters], L) -> Bounds1 = lists:zipwith(fun(Bound, Bucket) -> {Bound, Bucket} end, Bounds, BCounters), [{lists:zip(Labels, LabelValues), Bounds1, prometheus_time:maybe_convert_to_du(DU, ISum + FSum)}|L] end, [], LabelValuesMap). create_histogram(Name, Help, Data) -> prometheus_model_helpers:create_mf(Name, Help, histogram, ?MODULE, Data).