%% @doc
%% Helpers for working with Prometheus data model. For advanced users.
%% Probably will be used with {@link prometheus_collector}.
%% @end
-module(prometheus_model_helpers).
-export([metric_name/1,
create_mf/4,
create_mf/5,
gauge_metrics/1,
gauge_metric/1,
gauge_metric/2,
untyped_metrics/1,
untyped_metric/1,
untyped_metric/2,
boolean_metrics/1,
boolean_metric/1,
boolean_metric/2,
boolean_value/1,
counter_metrics/1,
counter_metric/1,
counter_metric/2,
summary_metrics/1,
summary_metric/1,
summary_metric/2,
summary_metric/3,
histogram_metrics/1,
histogram_metric/1,
histogram_metric/3,
histogram_metric/4,
label_pairs/1,
label_pair/1]).
-ifdef(TEST).
-export([filter_undefined_metrics/1,
ensure_mf_type/1,
ensure_binary_or_string/1]).
-endif.
-export_type([prometheus_boolean/0]).
-include("prometheus_model.hrl").
%%%===================================================================
%%% Types
%%%===================================================================
-type label_name() :: term().
-type label_value() :: term().
-type label() :: {label_name(), label_value()}.
-type labels() :: [labels()].
-type value() :: float() | integer() | undefined | infinity.
-type prometheus_boolean() :: boolean() | number() | list() | undefined.
-type gauge() :: value() | {value()} | {labels(), value()}.
-type counter() :: value() | {value()} | {labels(), value()}.
-type untyped() :: value() | {value()} | {labels(), value()}.
-type summary() :: {non_neg_integer(), value()} |
{labels(), non_neg_integer(), value()}.
-type buckets() :: nonempty_list({prometheus_buckets:bucket_bound(),
non_neg_integer()}).
-type histogram() :: {buckets(), non_neg_integer(), value()} |
{labels(), buckets(), non_neg_integer(), value()}.
-type pbool() :: prometheus_boolean() | {prometheus_boolean()} |
{labels(), prometheus_boolean()}.
-type tmetric() :: gauge() | counter() | untyped() | summary() |
histogram() | pbool().
-type metrics() :: tmetric() | [tmetric()].
%%%===================================================================
%%% Public API
%%%===================================================================
%% @doc
%% If `Name' is a list, looks for atoms and converts them to binaries.
%% Why iolists do not support atoms?
%% @end
-spec metric_name(Name) -> iolist() when
Name :: atom() | binary() | list(char() | iolist() | binary() | atom()).
metric_name(Name) ->
case Name of
_ when is_atom(Name) ->
atom_to_binary(Name, utf8);
_ when is_list(Name) ->
[
case is_atom(P) of
true -> atom_to_binary(P, utf8);
_ -> P
end
|| P <- Name];
_ ->
Name
end.
%% @doc
%% Create Metric Family of `Type', `Name' and `Help'.
%% `Collector:collect_metrics/2' callback will be called and expected to
%% return individual metrics list.
%% @end
-spec create_mf(Name, Help, Type, Metrics) -> MetricFamily when
Name :: prometheus_metric:name(),
Help :: prometheus_metric:help(),
Type :: atom(),
Metrics :: [prometheus_model:'Metric'()] |
prometheus_model:'Metric'() | metrics(),
MetricFamily :: prometheus_model:'MetricFamily'().
create_mf(Name, Help, Type, Metrics0) ->
Metrics = metrics_from_tuples(Type, Metrics0),
#'MetricFamily'{name = ensure_binary_or_string(Name),
help = ensure_binary_or_string(Help),
type = ensure_mf_type(Type),
metric = Metrics}.
%% @doc
%% Create Metric Family of `Type', `Name' and `Help'.
%% `Collector:collect_metrics/2' callback will be called and expected to
%% return individual metrics list.
%% @end
-spec create_mf(Name, Help, Type, Collector, CollectorData) -> MetricFamily when
Name :: prometheus_metric:name(),
Help :: prometheus_metric:help(),
Type :: atom(),
Collector :: prometheus_collector:collector(),
CollectorData :: prometheus_collector:data(),
MetricFamily :: prometheus_model:'MetricFamily'().
create_mf(Name, Help, Type, Collector, CollectorData) ->
create_mf(Name, Help, Type, Collector:collect_metrics(Name, CollectorData)).
%% @doc Equivalent to
%% {@link gauge_metric/1. `lists:map(fun gauge_metric/1, Values)'}.
%% @end
gauge_metrics(Values) -> lists:map(fun gauge_metric/1, Values).
%% @doc
%% Equivalent to
%% gauge_metric(Labels, Value).
%% @end
-spec gauge_metric(Gauge) -> prometheus_model:'Metric'() when
Gauge :: gauge().
gauge_metric({Labels, Value}) -> gauge_metric(Labels, Value);
gauge_metric({Value}) -> gauge_metric([], Value);
gauge_metric(Value) -> gauge_metric([], Value).
%% @doc
%% Creates gauge metric with `Labels' and `Value'.
%% @end
-spec gauge_metric(Labels, Value) -> prometheus_model:'Metric'() when
Labels :: labels(),
Value :: value().
gauge_metric(Labels, Value) ->
#'Metric'{label = label_pairs(Labels),
gauge = #'Gauge'{value = Value}}.
%% @doc Equivalent to
%% {@link untyped_metric/1. `lists:map(fun untyped_metric/1, Values)'}.
%% @end
untyped_metrics(Values) -> lists:map(fun untyped_metric/1, Values).
%% @doc
%% Equivalent to
%% untyped_metric(Labels, Value).
%% @end
-spec untyped_metric(Untyped) -> prometheus_model:'Metric'() when
Untyped :: untyped().
untyped_metric({Labels, Value}) -> untyped_metric(Labels, Value);
untyped_metric({Value}) -> untyped_metric([], Value);
untyped_metric(Value) -> untyped_metric([], Value).
%% @doc
%% Creates untyped metric with `Labels' and `Value'.
%% @end
-spec untyped_metric(Labels, Value) -> prometheus_model:'Metric'() when
Labels :: labels(),
Value :: value().
untyped_metric(Labels, Value) ->
#'Metric'{label = label_pairs(Labels),
untyped = #'Untyped'{value = Value}}.
%% @doc Equivalent to
%% {@link boolean_metric/1. `lists:map(fun boolean_metric/1, Values)'}.
%% @end
boolean_metrics(Values) -> lists:map(fun boolean_metric/1, Values).
%% @doc
%% Equivalent to
%% boolean_metric(Labels, Value).
%% @end
-spec boolean_metric(Boolean) -> prometheus_model:'Metric'() when
Boolean :: pbool().
boolean_metric({Labels, Value}) -> boolean_metric(Labels, Value);
boolean_metric({Value}) -> boolean_metric([], Value);
boolean_metric(Value) -> boolean_metric([], Value).
%% @doc
%% Creates boolean metric with `Labels' and `Value'.
%% @end
-spec boolean_metric(Labels, Value) -> prometheus_model:'Metric'() when
Labels :: labels(),
Value :: prometheus_boolean().
boolean_metric(Labels, Value0) ->
Value = boolean_value(Value0),
untyped_metric(Labels, Value).
%% @private
-spec boolean_value(Value) -> RealValue when
Value :: prometheus_boolean(),
RealValue :: undefined | 0 | 1.
boolean_value(Value) ->
case Value of
true -> 1;
false -> 0;
1 -> 1;
0 -> 0;
[] -> 0;
_ when is_number(Value) andalso Value > 0 -> 1;
_ when is_list(Value) -> 1;
undefined -> undefined;
_ -> erlang:error({invalid_value, Value, "value is not boolean"})
end.
%% @doc Equivalent to
%% {@link counter_metric/1. `lists:map(fun counter_metric/1, Specs)'}.
counter_metrics(Specs) -> lists:map(fun counter_metric/1, Specs).
%% @doc
%% Equivalent to
%% counter_metric(Labels, Value).
%% @end
-spec counter_metric(Spec) -> prometheus_model:'Metric'() when
Spec :: counter().
counter_metric({Labels, Value}) -> counter_metric(Labels, Value);
counter_metric({Value}) -> counter_metric([], Value);
counter_metric(Value) -> counter_metric([], Value).
%% @doc
%% Creates counter metric with `Labels' and `Value'.
%% @end
-spec counter_metric(Labels, Value) -> prometheus_model:'Metric'() when
Labels :: labels(),
Value :: value().
counter_metric(Labels, Value) ->
#'Metric'{label = label_pairs(Labels),
counter = #'Counter'{value = Value}}.
%% @doc Equivalent to
%% {@link summary_metric/1. `lists:map(fun summary_metric/1, Specs)'}.
summary_metrics(Specs) -> lists:map(fun summary_metric/1, Specs).
%% @doc
%% Equivalent to
%% summary_metric(Labels, Count, Sum).
%% @end
-spec summary_metric(Summary) -> prometheus_model:'Metric'() when
Summary :: summary().
summary_metric({Labels, Count, Sum}) -> summary_metric(Labels, Count, Sum);
summary_metric({Count, Sum}) -> summary_metric([], Count, Sum).
%% @equiv summary_metric([], Count, Sum)
summary_metric(Count, Sum) -> summary_metric([], Count, Sum).
%% @doc
%% Creates summary metric with `Labels', `Count' and `Sum'.
%% @end
-spec summary_metric(Labels, Count, Sum) -> prometheus_model:'Metric'() when
Labels :: labels(),
Count :: non_neg_integer(),
Sum :: value().
summary_metric(Labels, Count, Sum) ->
#'Metric'{label = label_pairs(Labels),
summary = #'Summary'{sample_count = Count,
sample_sum = Sum}}.
%% @doc Equivalent to
%% {@link histogram_metric/1. `lists:map(fun histogram_metric/1, Specs)'}.
%% @end
histogram_metrics(Specs) -> lists:map(fun histogram_metric/1, Specs).
%% @doc
%% Equivalent to
%%
%% histogram_metric(Labels, Buckets, Count, Sum).
%% @end
-spec histogram_metric(Histogram) -> prometheus_model:'Metric'() when
Histogram :: histogram().
histogram_metric({Labels, Buckets, Count, Sum}) ->
histogram_metric(Labels, Buckets, Count, Sum);
histogram_metric({Buckets, Count, Sum}) ->
histogram_metric([], Buckets, Count, Sum).
%% @equiv histogram_metric([], Buckets, Count, Sum)
histogram_metric(Buckets, Count, Sum) ->
histogram_metric([], Buckets, Count, Sum).
%% @doc
%% Creates histogram metric with `Labels', `Buckets', `Count' and `Sum'.
%% @end
-spec histogram_metric(Labels, Buckets, Count, Sum) -> Metric when
Labels :: labels(),
Buckets :: [{Bound, Count}],
Bound :: prometheus_buckets:bucket_bound(),
Count :: non_neg_integer(),
Sum :: value(),
Metric :: prometheus_model:'Metric'().
histogram_metric(Labels, Buckets, Count, Sum) ->
Label = label_pairs(Labels),
Bucket = histogram_buckets(Buckets),
#'Metric'{label = Label,
histogram = #'Histogram'{sample_count = Count,
sample_sum = Sum,
bucket = Bucket}}.
%% @doc Equivalent to
%% {@link label_pair/1. `lists:map(fun label_pair/1, Labels)'}.
%% @end
label_pairs(Labels) -> lists:map(fun label_pair/1, Labels).
%% @doc
%% Creates `prometheus_model:'LabelPair'()' from {Name, Value} tuple.
%% @end
-spec label_pair(label()) -> prometheus_model:'LabelPair'().
label_pair({Name, Value}) ->
#'LabelPair'{name = ensure_binary_or_string(Name),
value = ensure_binary_or_string(Value)}.
%%%===================================================================
%%% Private Parts
%%%===================================================================
%% @doc Equivalent to
%% {@link histogram_bucket/1. `lists:map(fun histogram_bucket/1, Specs)'}.
histogram_buckets(Specs) -> lists:map(fun histogram_bucket/1, Specs).
-spec histogram_bucket({Bound, Count}) -> Buckets when
Bound :: prometheus_buckets:bucket_bound(),
Count :: non_neg_integer(),
Buckets :: prometheus_model:'Bucket'().
histogram_bucket({Bound, Count}) ->
#'Bucket'{upper_bound = Bound,
cumulative_count = Count}.
metrics_from_tuples(Type, Metrics) ->
[metric_from_tuple(Type, Metric) ||
Metric <- filter_undefined_metrics(ensure_list(Metrics))].
metric_from_tuple(_, Metric) when is_record(Metric, 'Metric') ->
Metric;
metric_from_tuple(gauge, Metric) ->
gauge_metric(Metric);
metric_from_tuple(counter, Metric) ->
counter_metric(Metric);
metric_from_tuple(boolean, Metric) ->
boolean_metric(Metric);
metric_from_tuple(summary, Metric) ->
summary_metric(Metric);
metric_from_tuple(histogram, Metric) ->
histogram_metric(Metric);
metric_from_tuple(untyped, Metric) ->
untyped_metric(Metric).
-spec ensure_list(Val :: term()) -> list().
ensure_list(Val) when is_list(Val) -> Val;
ensure_list(Val) -> [Val].
%% @private
filter_undefined_metrics(Metrics) -> lists:filter(fun not_undefined/1, Metrics).
not_undefined(undefined) -> false;
not_undefined(_) -> true.
%% @private
-spec ensure_binary_or_string(Val :: term()) -> binary() | string().
ensure_binary_or_string(Val) when is_atom(Val) -> atom_to_binary(Val, utf8);
ensure_binary_or_string(Val) when is_list(Val) -> Val; %% FIXME: validate utf8
ensure_binary_or_string(Val) when is_binary(Val) -> Val;
ensure_binary_or_string(Val) ->
io_lib:format("~p", [Val]).
%% @private
-spec ensure_mf_type(atom()) -> atom().
ensure_mf_type(gauge) -> 'GAUGE';
ensure_mf_type(counter) -> 'COUNTER';
ensure_mf_type(summary) -> 'SUMMARY';
ensure_mf_type(histogram) -> 'HISTOGRAM';
ensure_mf_type(untyped) -> 'UNTYPED';
ensure_mf_type(boolean) -> 'UNTYPED';
ensure_mf_type(Type) -> erlang:error({invalid_metric_type, Type}).