%% @doc %% %% Serializes Prometheus registry using the latest %% [text format](http://bit.ly/2cxSuJP). %% %% Example output: %%
%%   # TYPE http_request_duration_milliseconds histogram
%%   # HELP http_request_duration_milliseconds Http Request execution time
%%   http_request_duration_milliseconds_bucket{method="post",le="100"} 0
%%   http_request_duration_milliseconds_bucket{method="post",le="300"} 1
%%   http_request_duration_milliseconds_bucket{method="post",le="500"} 3
%%   http_request_duration_milliseconds_bucket{method="post",le="750"} 4
%%   http_request_duration_milliseconds_bucket{method="post",le="1000"} 5
%%   http_request_duration_milliseconds_bucket{method="post",le="+Inf"} 6
%%   http_request_duration_milliseconds_count{method="post"} 6
%%   http_request_duration_milliseconds_sum{method="post"} 4350
%% 
%% @end -module(prometheus_text_format). -export([content_type/0, format/0, format/1, render_labels/1, escape_label_value/1 ]). -ifdef(TEST). -export([escape_metric_help/1, emit_mf_prologue/2, emit_mf_metrics/2 ]). -endif. -include("prometheus_model.hrl"). -behaviour(prometheus_format). -compile({inline, [add_brackets/1, render_label_pair/1]}). %%==================================================================== %% Macros %%==================================================================== %%==================================================================== %% Format API %%==================================================================== -spec content_type() -> binary(). %% @doc %% Returns content type of the latest [text format](http://bit.ly/2cxSuJP). %% @end content_type() -> <<"text/plain; version=0.0.4">>. %% @equiv format(default) -spec format() -> binary(). %% @doc %% Formats `default' registry using the latest text format. %% @end format() -> format(default). -spec format(Registry :: prometheus_registry:registry()) -> binary(). %% @doc %% Formats `Registry' using the latest text format. %% @end format(Registry) -> {ok, Fd} = ram_file:open("", [write, read, binary]), Callback = fun (_, Collector) -> registry_collect_callback(Fd, Registry, Collector) end, prometheus_registry:collect(Registry, Callback), file:write(Fd, "\n"), {ok, Size} = ram_file:get_size(Fd), {ok, Str} = file:pread(Fd, 0, Size), ok = file:close(Fd), Str. -spec escape_label_value(binary() | iolist()) -> binary(). %% @doc %% Escapes the backslash (\), double-quote ("), and line feed (\n) characters %% @end escape_label_value(LValue) when is_binary(LValue) -> case has_special_char(LValue) of true -> escape_string(fun escape_label_char/1, LValue); false -> LValue end; escape_label_value(LValue) when is_list(LValue) -> escape_label_value(iolist_to_binary(LValue)); escape_label_value(Value) -> erlang:error({invalid_value, Value}). %%==================================================================== %% Private Parts %%==================================================================== registry_collect_callback(Fd, Registry, Collector) -> Callback = fun (MF) -> emit_mf_prologue(Fd, MF), emit_mf_metrics(Fd, MF) end, prometheus_collector:collect_mf(Registry, Collector, Callback). %% @private emit_mf_prologue(Fd, #'MetricFamily'{name=Name, help=Help, type=Type}) -> Bytes = ["# TYPE ", Name, " ", string_type(Type), "\n# HELP ", Name, " ", escape_metric_help(Help), "\n"], file:write(Fd, Bytes). %% @private emit_mf_metrics(Fd, #'MetricFamily'{name=Name, metric = Metrics}) -> %% file:write/2 is an expensive operation, as it goes through a port driver. %% Instead a large chunk of bytes is being collected here, in a %% way that triggers binary append optimization in ERTS. Bytes = lists:foldl(fun (Metric, Blob) -> <> end, <<>>, Metrics), file:write(Fd, Bytes). render_metric(Name, #'Metric'{label=Labels, counter=#'Counter'{value=Value}}) -> render_series(Name, render_labels(Labels), Value); render_metric(Name, #'Metric'{label=Labels, gauge=#'Gauge'{value=Value}}) -> render_series(Name, render_labels(Labels), Value); render_metric(Name, #'Metric'{label=Labels, untyped=#'Untyped'{value=Value}}) -> render_series(Name, render_labels(Labels), Value); render_metric(Name, #'Metric'{label=Labels, summary=#'Summary'{sample_count=Count, sample_sum=Sum, quantile=Quantiles}}) -> LString = render_labels(Labels), Bytes1 = render_series([Name, "_count"], LString, Count), Bytes2 = <>, Bytes3 = lists:foldl(fun (#'Quantile'{quantile = QN, value = QV}, Blob) -> Val = render_series( [Name], render_labels( [LString, #'LabelPair'{name="quantile", value=io_lib:format("~p", [QN])}]), QV), <> end, Bytes2, Quantiles), Bytes3; render_metric(Name, #'Metric'{label=Labels, histogram=#'Histogram'{sample_count=Count, sample_sum=Sum, bucket=Buckets}}) -> %% StringLabels = labels_stringify(Labels), LString = render_labels(Labels), Bytes1 = lists:foldl(fun (Bucket, Blob) -> <> end, << >>, Buckets), Bytes2 = <>, Bytes3 = <>, Bytes3. emit_histogram_bucket(Name, LString, #'Bucket'{cumulative_count=BCount, upper_bound=BBound}) -> BLValue = bound_to_label_value(BBound), render_series([Name, "_bucket"], render_labels([LString, #'LabelPair'{name="le", value=BLValue}]), BCount). string_type('COUNTER') -> "counter"; string_type('GAUGE') -> "gauge"; string_type('SUMMARY') -> "summary"; string_type('HISTOGRAM') -> "histogram"; string_type('UNTYPED') -> "untyped". %% binary() in spec means 0 or more already rendered labels (name, %% escaped value), joined with "," in between -spec render_labels(binary() | [prometheus_model:'LabelPair'() | binary()]) -> binary(). -dialyzer({no_match, render_labels/1}). render_labels([]) -> <<>>; %% This clause is the reason for `-dialyzer` attr. It's an %% optimization, but it slightly violates the types automatically %% generated from protobufs. render_labels(B) when is_binary(B) -> B; render_labels([<<>>|Labels]) -> render_labels(Labels); render_labels([FirstLabel|Labels]) -> Start = << (render_label_pair(FirstLabel))/binary >>, B = lists:foldl(fun (<<>>, Acc) -> Acc; (Label, Acc) -> <> end, Start, Labels), <>. -spec render_label_pair(prometheus_model:'LabelPair'() | binary()) -> binary(). render_label_pair(B) when is_binary(B) -> B; render_label_pair(#'LabelPair'{name=Name, value=Value}) -> << (iolist_to_binary(Name))/binary, "=\"", (escape_label_value(Value))/binary, "\"" >>. add_brackets(<<>>) -> <<>>; add_brackets(LString) -> <<"{", LString/binary, "}">>. render_series(Name, LString, undefined) -> <<(iolist_to_binary(Name))/binary, (add_brackets(LString))/binary, " NaN\n">>; render_series(Name, LString, Value) when is_integer(Value) -> <<(iolist_to_binary(Name))/binary, (add_brackets(LString))/binary, " ", (integer_to_binary(Value))/binary , "\n">>; render_series(Name, LString, Value) -> <<(iolist_to_binary(Name))/binary, (add_brackets(LString))/binary, " ", (iolist_to_binary(io_lib:format("~p", [Value])))/binary , "\n">>. %% @private escape_metric_help(Help) -> escape_string(fun escape_help_char/1, Help). %% @private escape_help_char($\\ = X) -> <>; escape_help_char($\n) -> <<$\\, $n>>; escape_help_char(X) -> <>. bound_to_label_value(Bound) when is_integer(Bound) -> integer_to_list(Bound); bound_to_label_value(Bound) when is_float(Bound) -> float_to_list(Bound); bound_to_label_value(infinity) -> "+Inf". %% @private escape_label_char($\\ = X) -> <>; escape_label_char($\n) -> <<$\\, $n>>; escape_label_char($" = X) -> <<$\\, X>>; escape_label_char(X) -> <>. %% @perivate -spec has_special_char(binary()) -> boolean(). has_special_char(<>) when C =:= $\\; C =:= $\n; C =:= $" -> true; has_special_char(<<_:8, Rest/bitstring>>) -> has_special_char(Rest); has_special_char(<<>>) -> false. %% @private escape_string(Fun, Str) when is_binary(Str) -> << <<(Fun(X))/binary>> || <> <= Str >>; escape_string(Fun, Str) -> escape_string(Fun, iolist_to_binary(Str)).