%% @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) ->
<