Skip to content

Commit 3b631e2

Browse files
committed
Support for time_s and time_ms
1 parent 5e5fee2 commit 3b631e2

File tree

2 files changed

+138
-29
lines changed

2 files changed

+138
-29
lines changed

src/seshat.erl

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
-opaque group_ref() :: ets:tid().
2828
-type name() :: term().
2929

30+
-type metric_type() :: counter | gauge | ratio | time_s | time_ms.
31+
32+
-type prometheus_type() :: counter | gauge.
33+
3034
-type field_spec() :: {Name :: atom(), Index :: pos_integer(),
31-
Type :: counter | gauge | ratio, Help :: string()}.
35+
Type :: metric_type(), Help :: string()}.
3236

3337
-type fields_spec() :: [field_spec()] | {persistent_term, term()}.
3438

3539
-type format_result() :: #{Name :: atom() =>
36-
#{type => counter | gauge,
40+
#{type => prometheus_type(),
3741
help => string(),
3842
values => #{labels() => number()}}}.
3943

@@ -298,6 +302,8 @@ format_fields(Fields, CRef, Labels, Acc) ->
298302
fun ({Name, Index, Type, Help}, Acc0) ->
299303
ComputedType = case Type of
300304
ratio -> gauge;
305+
time_s -> gauge;
306+
time_ms -> gauge;
301307
Other -> Other
302308
end,
303309
InitialMetric = #{type => ComputedType,
@@ -306,8 +312,14 @@ format_fields(Fields, CRef, Labels, Acc) ->
306312
MetricAcc = maps:get(Name, Acc0, InitialMetric),
307313
ValuesAcc = maps:get(values, MetricAcc),
308314
ComputedValue = case Type of
309-
ratio -> counters:get(CRef, Index) / 100;
310-
_ -> counters:get(CRef, Index)
315+
ratio ->
316+
counters:get(CRef, Index) / 100;
317+
time_ms ->
318+
counters:get(CRef, Index) / 1000; % ms to s
319+
time_s ->
320+
counters:get(CRef, Index) * 1.0; % ensure float
321+
_ ->
322+
counters:get(CRef, Index)
311323
end,
312324
ValuesAcc1 = ValuesAcc#{Labels => ComputedValue},
313325
MetricAcc1 = MetricAcc#{values => ValuesAcc1},
@@ -360,34 +372,50 @@ text_format_fields(Fields, CRef, Labels, PrefixBin, Acc) ->
360372
% a given metric, so we accumulate in a map, with the metric name as key
361373
lists:foldl(
362374
fun ({Name, Index, Type, Help}, Acc0) ->
363-
ComputedType = case Type of
364-
ratio -> gauge;
365-
Other -> Other
366-
end,
367-
ComputedUnit = case Type of
368-
ratio -> <<"_ratio">>;
369-
_ -> <<"">>
370-
end,
375+
PromType = prometheus_type(Type),
376+
Suffix = prometheus_suffix(Type),
371377
ComputedValue = case Type of
372-
ratio -> Ratio = counters:get(CRef, Index) / 100,
373-
Ratio1= float_to_list(Ratio, [{decimals, 2}, compact]),
374-
list_to_binary(Ratio1)
375-
;
376-
_ -> integer_to_binary(counters:get(CRef, Index))
378+
ratio ->
379+
Value = counters:get(CRef, Index) / 100,
380+
Formatted = float_to_list(Value, [{decimals, 2}, compact]),
381+
list_to_binary(Formatted);
382+
time_ms ->
383+
Value = counters:get(CRef, Index) / 1000, % ms to s
384+
Formatted = float_to_list(Value, [{decimals, 3}, compact]),
385+
list_to_binary(Formatted);
386+
time_s ->
387+
Value = counters:get(CRef, Index) * 1.0, % ensure float
388+
Formatted = float_to_list(Value, [{decimals, 1}, compact]),
389+
list_to_binary(Formatted);
390+
_ -> % counter or gauge
391+
integer_to_binary(counters:get(CRef, Index))
377392
end,
378-
NameBin = <<PrefixBin/binary, (atom_to_binary(Name, utf8))/binary, ComputedUnit/binary>>,
393+
NameBin = <<PrefixBin/binary, (atom_to_binary(Name, utf8))/binary, Suffix/binary>>,
379394
Line = <<NameBin/binary, "{", LabelsBin/binary, "} ", ComputedValue/binary>>,
380395
case maps:get(Name, Acc0, <<>>) of
381396
<<>> ->
382397
HelpLine = <<"# HELP ", NameBin/binary, <<" ">>/binary, (list_to_binary(Help))/binary>>,
383-
TypeBin = atom_to_binary(ComputedType, utf8),
398+
TypeBin = atom_to_binary(PromType, utf8),
384399
TypeLine = <<"# TYPE ", NameBin/binary, <<" ">>/binary, TypeBin/binary>>,
385400
Acc0#{Name => <<HelpLine/binary, <<"\n">>/binary, TypeLine/binary, <<"\n">>/binary, Line/binary>>};
386401
Lines ->
387402
Acc0#{Name => <<Lines/binary, <<"\n">>/binary, Line/binary>>}
388403
end
389404
end, Acc, Fields).
390405

406+
-spec prometheus_type(metric_type()) -> prometheus_type().
407+
prometheus_type(counter) -> counter;
408+
prometheus_type(gauge) -> gauge;
409+
prometheus_type(ratio) -> gauge;
410+
prometheus_type(time_s) -> gauge;
411+
prometheus_type(time_ms) -> gauge.
412+
413+
-spec prometheus_suffix(metric_type()) -> binary().
414+
prometheus_suffix(ratio) -> <<"_ratio">>;
415+
prometheus_suffix(time_s) -> <<"_seconds">>;
416+
prometheus_suffix(time_ms) -> <<"_seconds">>;
417+
prometheus_suffix(_) -> <<"">>.
418+
391419
label_value_to_binary(Value) when is_atom(Value) ->
392420
atom_to_binary(Value, utf8);
393421
label_value_to_binary(Value) when is_list(Value) ->

test/seshat_test.erl

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test_suite_test_() ->
2424
fun format_one/0,
2525
fun format_with_many_labels/0,
2626
fun format_ratio/0,
27+
fun format_time_metrics/0,
2728
fun format_selected_metrics/0,
2829
fun text_format_selected_metrics/0,
2930
fun invalid_fields/0 ]}.
@@ -92,7 +93,7 @@ counters_with_persistent_term_field_spec() ->
9293
ok.
9394

9495
format_group() ->
95-
Group = widgets,
96+
Group = ?FUNCTION_NAME,
9697
Counters = [{reads, 1, counter, "Total reads"}],
9798
seshat:new_group(Group),
9899
seshat:new(Group, widget1, Counters, #{component => widget1}),
@@ -107,7 +108,7 @@ format_group() ->
107108
ok.
108109

109110
format_one() ->
110-
Group = widgets,
111+
Group = ?FUNCTION_NAME,
111112
Counters = [{reads, 1, counter, "Total reads"}],
112113
seshat:new_group(Group),
113114
seshat:new(Group, widget1, Counters, #{component => widget1}),
@@ -120,7 +121,7 @@ format_one() ->
120121
ok.
121122

122123
format_with_many_labels() ->
123-
Group = widgets,
124+
Group = ?FUNCTION_NAME,
124125
Counters = [{reads, 1, counter, "Total reads"}],
125126
seshat:new_group(Group),
126127
seshat:new(Group, widget1, Counters, #{component => "widget1", status => up}),
@@ -136,7 +137,7 @@ format_with_many_labels() ->
136137
ok.
137138

138139
format_selected_metrics() ->
139-
Group = widgets,
140+
Group = ?FUNCTION_NAME,
140141
Counters = [
141142
{reads, 1, counter, "Total reads"},
142143
{writes, 2, counter, "Total writes"},
@@ -168,7 +169,7 @@ invalid_fields() ->
168169
ok.
169170

170171
format_ratio() ->
171-
Group = widgets,
172+
Group = ?FUNCTION_NAME,
172173
Counters = [{pings, 1, ratio, "Some ratio that happens to be 0%"},
173174
{pongs, 2, ratio, "Some ratio that happens to be 17%"},
174175
{pangs, 3, ratio, "Some ratio that happens to be 33%"},
@@ -197,27 +198,97 @@ format_ratio() ->
197198
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
198199
ok.
199200

201+
format_time_metrics() ->
202+
Group = ?FUNCTION_NAME,
203+
Counters = [
204+
{job_duration, 2, time_s, "Job duration"},
205+
{short_latency, 3, time_ms, "Short latency"},
206+
{long_latency, 1, time_ms, "Request latency"}
207+
],
208+
seshat:new_group(Group),
209+
Labels = #{component => test},
210+
seshat:new(Group, test_component, Counters, Labels),
211+
212+
% Set values (1500 ms, 30 s, 5 ms)
213+
set_value(Group, test_component, job_duration, 30),
214+
set_value(Group, test_component, short_latency, 5),
215+
set_value(Group, test_component, long_latency, 1500),
216+
217+
MapFormat = seshat:format(Group),
218+
ExpectedMapFormat = #{
219+
job_duration => #{type => gauge,
220+
help => "Job duration",
221+
values => #{Labels => 30.0}},
222+
short_latency => #{type => gauge,
223+
help => "Short latency",
224+
values => #{Labels => 0.005}},
225+
long_latency => #{type => gauge,
226+
help => "Request latency",
227+
values => #{Labels => 1.5}}
228+
},
229+
?assertEqual(ExpectedMapFormat, MapFormat),
230+
231+
Prefix = "myapp",
232+
MetricNames = [job_duration, short_latency, long_latency], % Added new metric name
233+
ResultAsList = binary_to_list(seshat:text_format(Group, Prefix, MetricNames)),
234+
235+
% Expected format needs sorting because order isn't guaranteed
236+
ExpectedLines = [
237+
"# HELP myapp_job_duration_seconds Job duration",
238+
"# TYPE myapp_job_duration_seconds gauge",
239+
"myapp_job_duration_seconds{component=\"test\"} 30.0",
240+
"# HELP myapp_short_latency_seconds Short latency",
241+
"# TYPE myapp_short_latency_seconds gauge",
242+
"myapp_short_latency_seconds{component=\"test\"} 0.005",
243+
"# HELP myapp_long_latency_seconds Request latency",
244+
"# TYPE myapp_long_latency_seconds gauge",
245+
"myapp_long_latency_seconds{component=\"test\"} 1.5"
246+
],
247+
ExpectedSortedText = lists:sort(ExpectedLines),
248+
249+
% Split and sort the actual result for comparison
250+
ResultLines = string:split(ResultAsList, "\n", all),
251+
FilteredResultLines = [Line || Line <- ResultLines, Line /= ""],
252+
SortedResultText = lists:sort(FilteredResultLines),
253+
254+
?assertEqual(ExpectedSortedText, SortedResultText),
255+
256+
ok.
257+
200258
text_format_selected_metrics() ->
201259
Group = widgets,
202260
Counters = [
203261
{reads, 1, counter, "Total reads"},
204262
{writes, 2, counter, "Total writes"},
205-
{cached, 3, ratio, "Ratio of things served from cache"}
206-
],
263+
{cached, 3, ratio, "Ratio of things served from cache"},
264+
{latency, 4, time_ms, "Latency"},
265+
{duration, 5, time_s, "Duration"},
266+
{npc, 6, gauge, "A metric we don't request in a call to text_format/3"}
267+
],
207268
seshat:new_group(Group),
208269
seshat:new(Group, thing1, Counters, #{component => "thing1", version => "1.2.3"}),
209270
seshat:new(Group, thing2, Counters, #{component => "thing2", some_atom => atom_value}),
210271
seshat:new(Group, thing3, Counters, #{component => "thing3", some_binary => <<"binary_value">>}),
211272
set_value(Group, thing1, reads, 1),
212273
set_value(Group, thing1, writes, 2),
213274
set_value(Group, thing1, cached, 10),
275+
set_value(Group, thing1, latency, 5),
276+
set_value(Group, thing1, duration, 123),
277+
set_value(Group, thing1, npc, 1), % to be ignored
214278
set_value(Group, thing2, reads, 3),
215279
set_value(Group, thing2, writes, 4),
216280
set_value(Group, thing2, cached, 100),
281+
set_value(Group, thing2, latency, 6),
282+
set_value(Group, thing2, duration, 234),
283+
set_value(Group, thing2, npc, 1), % to be ignored
217284
set_value(Group, thing3, reads, 1234),
218285
set_value(Group, thing3, writes, 4321),
219286
set_value(Group, thing3, cached, 17),
220-
PrometheusFormat = binary_to_list(seshat:text_format(Group, "acme", [reads, writes, cached])),
287+
set_value(Group, thing3, latency, 7),
288+
set_value(Group, thing3, duration, 345),
289+
set_value(Group, thing3, npc, 1), % to be ignored
290+
291+
ResultAsList = binary_to_list(seshat:text_format(Group, "acme", [reads, writes, cached, latency, duration])),
221292
ExpectedPrometheusFormat = "# HELP acme_reads Total reads\n"
222293
"# TYPE acme_reads counter\n"
223294
"acme_reads{version=\"1.2.3\",component=\"thing1\"} 1\n"
@@ -232,9 +303,19 @@ text_format_selected_metrics() ->
232303
"# TYPE acme_cached_ratio gauge\n"
233304
"acme_cached_ratio{version=\"1.2.3\",component=\"thing1\"} 0.1\n"
234305
"acme_cached_ratio{component=\"thing2\",some_atom=\"atom_value\"} 1.0\n"
235-
"acme_cached_ratio{component=\"thing3\",some_binary=\"binary_value\"} 0.17\n",
306+
"acme_cached_ratio{component=\"thing3\",some_binary=\"binary_value\"} 0.17\n"
307+
"# HELP acme_latency_seconds Latency\n"
308+
"# TYPE acme_latency_seconds gauge\n"
309+
"acme_latency_seconds{version=\"1.2.3\",component=\"thing1\"} 0.005\n"
310+
"acme_latency_seconds{component=\"thing2\",some_atom=\"atom_value\"} 0.006\n"
311+
"acme_latency_seconds{component=\"thing3\",some_binary=\"binary_value\"} 0.007\n"
312+
"# HELP acme_duration_seconds Duration\n"
313+
"# TYPE acme_duration_seconds gauge\n"
314+
"acme_duration_seconds{version=\"1.2.3\",component=\"thing1\"} 123.0\n"
315+
"acme_duration_seconds{component=\"thing2\",some_atom=\"atom_value\"} 234.0\n"
316+
"acme_duration_seconds{component=\"thing3\",some_binary=\"binary_value\"} 345.0\n",
236317

237-
?assertEqual(ExpectedPrometheusFormat, PrometheusFormat),
318+
?assertEqual(ExpectedPrometheusFormat, ResultAsList),
238319
ok.
239320

240321
%% test helpers

0 commit comments

Comments
 (0)