Skip to content

Commit 72b907c

Browse files
committed
Add support to generate a Promethus exposition format output
seshat:text_format/3 produces a binary that contains the text representation of the metrics in the Prometheus exposition format, so that it can be returned as-is by a Prometheus endpoint
1 parent 891b340 commit 72b907c

File tree

3 files changed

+149
-10
lines changed

3 files changed

+149
-10
lines changed

rebar3.crashdump

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Error: terminated
2+
[{io,format,
3+
["rebar ~ts on Erlang/OTP ~ts Erts ~ts~n",
4+
["3.24.0+build.5437.ref5495da14","27","15.2.3"]],
5+
[{file,"io.erl"},
6+
{line,198},
7+
{error_info,#{cause => {io,terminated},module => erl_stdlib_errors}}]},
8+
{rebar_prv_version,do,1,
9+
[{file,"/Users/mkuratczyk/workspace/rebar3/apps/rebar/src/rebar_prv_version.erl"},
10+
{line,36}]},
11+
{rebar_core,do,2,
12+
[{file,"/Users/mkuratczyk/workspace/rebar3/apps/rebar/src/rebar_core.erl"},
13+
{line,155}]},
14+
{rebar3,run_aux,2,
15+
[{file,"/Users/mkuratczyk/workspace/rebar3/apps/rebar/src/rebar3.erl"},
16+
{line,212}]},
17+
{rebar3,main,1,
18+
[{file,"/Users/mkuratczyk/workspace/rebar3/apps/rebar/src/rebar3.erl"},
19+
{line,66}]},
20+
{escript,run,2,[{file,"escript.erl"},{line,904}]},
21+
{escript,start,1,[{file,"escript.erl"},{line,418}]},
22+
{init,start_it,1,[]}]
23+

src/seshat.erl

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
delete/2,
2020
format/1,
2121
format/2,
22+
text_format/3,
2223
format_one/2
2324
]).
2425

@@ -310,3 +311,77 @@ format_fields(Fields, CRef, Labels, Acc) ->
310311
MetricAcc1 = MetricAcc#{values => ValuesAcc1},
311312
Acc0#{Name => MetricAcc1}
312313
end, Acc, Fields).
314+
315+
%% @doc Return a Prometheus-formated text (as a binary),
316+
%% which can be directly returned by a Prometheus endpoint.
317+
%% The returned binary has the following structure:
318+
%% prefix_name_unit{label_key="value_value",...} Value
319+
%%
320+
%% Units are automatically appended based on the metric type.
321+
%%
322+
%% @param Group the name of an existing group
323+
%% @param Names the list of metrics to return
324+
%%
325+
-spec text_format(group(), string(), [atom()]) -> binary().
326+
text_format(Group, Prefix, Names) when is_list(Names) ->
327+
PrefixBin = list_to_binary(Prefix ++ "_"),
328+
Metrics = ets:foldl(fun
329+
({_Name, _CRef, _FieldSpec, Labels}, Acc) when map_size(Labels) == 0 ->
330+
%% skip metrics with no labels; at some point we might want to export them
331+
Acc;
332+
({_Name, CRef, FieldSpec, Labels}, Acc) ->
333+
Fields0 = resolve_fields_spec(FieldSpec),
334+
Fields = lists:filter(fun (F) -> lists:member(element(1, F), Names) end, Fields0),
335+
text_format_fields(Fields, CRef, Labels, PrefixBin, Acc)
336+
end, #{}, seshat_counters_server:get_table(Group)),
337+
MetricsBin = maps:fold(fun(_Name, Lines, Acc) ->
338+
<<Acc/binary, Lines/binary, <<"\n">>/binary>>
339+
end, <<>>, Metrics),
340+
MetricsBin.
341+
342+
text_format_fields(Fields, CRef, Labels, PrefixBin, Acc) ->
343+
LabelsList = maps:to_list(Labels),
344+
% transform the map of labels into "label1=value1,label2=value2"
345+
LabelsBin0 = lists:foldl(
346+
fun ({Name, Value}, LabelsAcc) ->
347+
LabelKey = atom_to_binary(Name, utf8),
348+
LabelValue = list_to_binary(Value),
349+
<<LabelsAcc/binary, LabelKey/binary, "=\"", LabelValue/binary, "\",">>
350+
end, <<>>, LabelsList),
351+
% remove the final comma
352+
LabelsBin = case LabelsBin0 of
353+
<<>> -> <<>>;
354+
_ -> binary:part(LabelsBin0, 0, byte_size(LabelsBin0) - 1)
355+
end,
356+
% produce the lines for each metric; we are itearting by object, but Prometheus
357+
% output should be sorted by metric name with one HELP and one TYPE line for
358+
% a given metric, so we accumulate in a map, with the metric name as key
359+
lists:foldl(
360+
fun ({Name, Index, Type, Help}, Acc0) ->
361+
ComputedType = case Type of
362+
ratio -> gauge;
363+
Other -> Other
364+
end,
365+
ComputedUnit = case Type of
366+
ratio -> <<"_ratio">>;
367+
_ -> <<"">>
368+
end,
369+
ComputedValue = case Type of
370+
ratio -> Ratio = counters:get(CRef, Index) / 100,
371+
Ratio1= float_to_list(Ratio, [{decimals, 2}, compact]),
372+
list_to_binary(Ratio1)
373+
;
374+
_ -> integer_to_binary(counters:get(CRef, Index))
375+
end,
376+
NameBin = <<PrefixBin/binary, (atom_to_binary(Name, utf8))/binary, ComputedUnit/binary>>,
377+
Line = <<NameBin/binary, "{", LabelsBin/binary, "} ", ComputedValue/binary>>,
378+
case maps:get(Name, Acc0, <<>>) of
379+
<<>> ->
380+
HelpLine = <<"# HELP ", NameBin/binary, <<" ">>/binary, (list_to_binary(Help))/binary>>,
381+
TypeBin = atom_to_binary(ComputedType, utf8),
382+
TypeLine = <<"# TYPE ", NameBin/binary, <<" ">>/binary, TypeBin/binary>>,
383+
Acc0#{Name => <<HelpLine/binary, <<"\n">>/binary, TypeLine/binary, <<"\n">>/binary, Line/binary>>};
384+
Lines ->
385+
Acc0#{Name => <<Lines/binary, <<"\n">>/binary, Line/binary>>}
386+
end
387+
end, Acc, Fields).

test/seshat_test.erl

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ test_suite_test_() ->
2020
fun cleanup/1,
2121
[ fun overview/0,
2222
fun counters_with_persistent_term_field_spec/0,
23-
fun prometheus_format_group/0,
24-
fun prometheus_format_one/0,
25-
fun prometheus_format_with_many_labels/0,
26-
fun prometheus_format_ratio/0,
27-
fun prometheus_format_selected_metrics/0,
23+
fun format_group/0,
24+
fun format_one/0,
25+
fun format_with_many_labels/0,
26+
fun format_ratio/0,
27+
fun format_selected_metrics/0,
28+
fun text_format_selected_metrics/0,
2829
fun invalid_fields/0 ]}.
2930

3031
overview() ->
@@ -90,7 +91,7 @@ counters_with_persistent_term_field_spec() ->
9091

9192
ok.
9293

93-
prometheus_format_group() ->
94+
format_group() ->
9495
Group = widgets,
9596
Counters = [{reads, 1, counter, "Total reads"}],
9697
seshat:new_group(Group),
@@ -105,7 +106,7 @@ prometheus_format_group() ->
105106
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
106107
ok.
107108

108-
prometheus_format_one() ->
109+
format_one() ->
109110
Group = widgets,
110111
Counters = [{reads, 1, counter, "Total reads"}],
111112
seshat:new_group(Group),
@@ -118,7 +119,7 @@ prometheus_format_one() ->
118119
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
119120
ok.
120121

121-
prometheus_format_with_many_labels() ->
122+
format_with_many_labels() ->
122123
Group = widgets,
123124
Counters = [{reads, 1, counter, "Total reads"}],
124125
seshat:new_group(Group),
@@ -134,7 +135,7 @@ prometheus_format_with_many_labels() ->
134135
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
135136
ok.
136137

137-
prometheus_format_selected_metrics() ->
138+
format_selected_metrics() ->
138139
Group = widgets,
139140
Counters = [
140141
{reads, 1, counter, "Total reads"},
@@ -166,7 +167,7 @@ invalid_fields() ->
166167

167168
ok.
168169

169-
prometheus_format_ratio() ->
170+
format_ratio() ->
170171
Group = widgets,
171172
Counters = [{pings, 1, ratio, "Some ratio that happens to be 0%"},
172173
{pongs, 2, ratio, "Some ratio that happens to be 17%"},
@@ -196,6 +197,46 @@ prometheus_format_ratio() ->
196197
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
197198
ok.
198199

200+
text_format_selected_metrics() ->
201+
Group = widgets,
202+
Counters = [
203+
{reads, 1, counter, "Total reads"},
204+
{writes, 2, counter, "Total writes"},
205+
{cached, 3, ratio, "Ratio of things served from cache"}
206+
],
207+
seshat:new_group(Group),
208+
seshat:new(Group, thing1, Counters, #{component => "thing1", version => "1.2.3"}),
209+
seshat:new(Group, thing2, Counters, #{component => "thing2"}),
210+
seshat:new(Group, thing3, Counters, #{component => "thing3"}),
211+
set_value(Group, thing1, reads, 1),
212+
set_value(Group, thing1, writes, 2),
213+
set_value(Group, thing1, cached, 10),
214+
set_value(Group, thing2, reads, 3),
215+
set_value(Group, thing2, writes, 4),
216+
set_value(Group, thing2, cached, 100),
217+
set_value(Group, thing3, reads, 1234),
218+
set_value(Group, thing3, writes, 4321),
219+
set_value(Group, thing3, cached, 17),
220+
PrometheusFormat = binary_to_list(seshat:text_format(Group, "acme", [reads, writes, cached])),
221+
ExpectedPrometheusFormat = "# HELP acme_reads Total reads\n"
222+
"# TYPE acme_reads counter\n"
223+
"acme_reads{version=\"1.2.3\",component=\"thing1\"} 1\n"
224+
"acme_reads{component=\"thing2\"} 3\n"
225+
"acme_reads{component=\"thing3\"} 1234\n"
226+
"# HELP acme_writes Total writes\n"
227+
"# TYPE acme_writes counter\n"
228+
"acme_writes{version=\"1.2.3\",component=\"thing1\"} 2\n"
229+
"acme_writes{component=\"thing2\"} 4\n"
230+
"acme_writes{component=\"thing3\"} 4321\n"
231+
"# HELP acme_cached_ratio Ratio of things served from cache\n"
232+
"# TYPE acme_cached_ratio gauge\n"
233+
"acme_cached_ratio{version=\"1.2.3\",component=\"thing1\"} 0.1\n"
234+
"acme_cached_ratio{component=\"thing2\"} 1.0\n"
235+
"acme_cached_ratio{component=\"thing3\"} 0.17\n",
236+
237+
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
238+
ok.
239+
199240
%% test helpers
200241

201242
set_value(Group, Id, Name, Value) ->

0 commit comments

Comments
 (0)