Skip to content

Commit

Permalink
Support global and prerendered labels together (#149)
Browse files Browse the repository at this point in the history
Without this change, a collector that pre-renders labels (like RabbitMQ)
will return `<<>>` in case there are no labels at all. That will create an
an improper list when an empty binary is appended to a list of global
labels. Rather than dealing with improper list later, let's just avoid
them.

Co-authored-by: Michal Kuratczyk <[email protected]>
  • Loading branch information
binarin and mkuratczyk authored Jan 11, 2023
1 parent 57774ac commit 4e4a9fd
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/formats/prometheus_text_format.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
-module(prometheus_text_format).
-export([content_type/0,
format/0,
format/1]).
format/1,
render_labels/1]).

-ifdef(TEST).
-export([escape_metric_help/1,
Expand Down
37 changes: 27 additions & 10 deletions src/prometheus_collector.erl
Original file line number Diff line number Diff line change
Expand Up @@ -136,23 +136,40 @@ enabled_collectors() ->
{ok, Collectors} -> catch_default_collectors(Collectors)
end).

%% pre-rendering optimization for the text format violates type constraints, but we still want it. So here is the
%% separate function with relevant dialyzer check disabled.
-dialyzer({no_match, global_labels_callback_wrapper/2}).
-spec global_labels_callback_wrapper([], Callback) -> Callback when
Callback :: collect_mf_callback().
global_labels_callback_wrapper(GlobalLabels0, Callback) ->
GlobalLabels = prometheus_model_helpers:label_pairs(GlobalLabels0),
GlobalLabelsPrerendered = prometheus_text_format:render_labels(GlobalLabels),
fun (MF=#'MetricFamily'{metric=Metrics0}) ->
Metrics =
[case ML of
%% empty binary singals to us that an app knows what it's doing,
%% so we can optimize for the text format
<<>> -> M#'Metric'{label = GlobalLabelsPrerendered};

<<_/binary>> when GlobalLabelsPrerendered =:= <<>> -> M;
<<_/binary>> -> M#'Metric'{label = <<GlobalLabelsPrerendered/binary, ",", ML/binary>>};
_ -> M#'Metric'{label = GlobalLabels ++ ML}
end
|| M=#'Metric'{label=ML} <- Metrics0],
Callback(MF#'MetricFamily'{metric=Metrics})
end.

%% @doc Calls `Callback' for each MetricFamily of this collector.
-spec collect_mf(Registry, Collector, Callback) -> ok when
Registry :: prometheus_registry:registry(),
Collector :: collector(),
Callback :: collect_mf_callback().
collect_mf(Registry, Collector, Callback0) ->
Callback = case application:get_env(prometheus, global_labels) of
undefined ->
Callback0;
{ok, Labels0} ->
Labels = prometheus_model_helpers:label_pairs(Labels0),
fun (MF=#'MetricFamily'{metric=Metrics0}) ->
Metrics = [M#'Metric'{label=Labels ++ ML}
|| M=#'Metric'{label=ML} <- Metrics0],
Callback0(MF#'MetricFamily'{metric=Metrics})
end
end,
undefined -> Callback0;
{ok, []} -> Callback0;
{ok, GlobalLabels} -> global_labels_callback_wrapper(GlobalLabels, Callback0)
end,
ok = Collector:collect_mf(Registry, Callback).

%%====================================================================
Expand Down
94 changes: 94 additions & 0 deletions test/eunit/prometheus_collector_global_labels_test.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
-module(prometheus_collector_global_labels_test).

-include("prometheus_model.hrl").
-include_lib("eunit/include/eunit.hrl").

-export([collect_mf/2, collect_metrics/2, deregister_cleanup/1]).

deregister_cleanup(_) ->
ok.

collect_mf(_Registry, Callback) ->
Callback(
prometheus_model_helpers:create_mf(
<<"prerendered_mf">>, <<"Help text">>, counter, ?MODULE,
{counter_prerendered, 5})),
Callback(
prometheus_model_helpers:create_mf(
<<"regular_mf">>, <<"Help text">>, counter, ?MODULE,
{counter, 5})),
Callback(
prometheus_model_helpers:create_mf(
<<"no_label_mf">>, <<"Help text">>, counter, ?MODULE,
{counter_no_label, 5})),

%% Empty binary can be a sign of an empty pre-rendered list
Callback(
prometheus_model_helpers:create_mf(
<<"no_label_prerendered_mf">>, <<"Help text">>, counter, ?MODULE,
{counter_no_label_prerendered, 5})),
ok.

collect_metrics(_, {counter_no_label, Value}) ->
[prometheus_model_helpers:counter_metric([], Value)];
collect_metrics(_, {counter_no_label_prerendered, Value}) ->
[prometheus_model_helpers:counter_metric(<<>>, Value)];
collect_metrics(_, {counter_prerendered, Value}) ->
[prometheus_model_helpers:counter_metric(<<"label=\"prerendered\"">>, Value)];
collect_metrics(_, {_, Value}) ->
[prometheus_model_helpers:counter_metric([{label, regular}], Value)].

global_labels_prerendering_test() ->
MFList =
try
prometheus:start(),
prometheus_registry:register_collectors([?MODULE]),
application:set_env(prometheus, global_labels, [{node, some_value1}]),
prometheus_collector:collect_mf_to_list(?MODULE)
after
application:unset_env(prometheus, global_labels),
prometheus_registry:deregister_collector(?MODULE)
end,
?assertEqual([[<<"node=\"some_value1\",label=\"prerendered\"">>]], get_labels(MFList, prerendered_mf)),
?assertEqual([[[#'LabelPair'{name = <<"node">>, value = <<"some_value1">>},
#'LabelPair'{name = <<"label">>, value = <<"regular">>}]]],
get_labels(MFList, regular_mf)),
ok.

empty_global_labels_prerendering_test() ->
MFList =
try
prometheus:start(),
prometheus_registry:register_collectors([?MODULE]),
application:set_env(prometheus, global_labels, []),
prometheus_collector:collect_mf_to_list(?MODULE)
after
application:unset_env(prometheus, global_labels),
prometheus_registry:deregister_collector(?MODULE)
end,
?assertEqual([[[]]], get_labels(MFList, no_label_mf)),
ok.

empty_labels_plus_global_labels_prerendering_test() ->
MFList =
try
prometheus:start(),
prometheus_registry:register_collectors([?MODULE]),
application:set_env(prometheus, global_labels, [{node, some_value2}]),
prometheus_collector:collect_mf_to_list(?MODULE)
after
application:unset_env(prometheus, global_labels),
prometheus_registry:deregister_collector(?MODULE)
end,
?assertEqual([[<<"node=\"some_value2\"">>]], get_labels(MFList, no_label_prerendered_mf)),
ok.

get_labels(MFList, MFName) ->
MFNameB = atom_to_binary(MFName, latin1),
lists:filtermap(fun
(#'MetricFamily'{name = MFName1, metric = Metrics}) when MFName1 =:= MFNameB ->
{true, [Label|| #'Metric'{label = Label} <- Metrics ]};
(_) ->
false
end, MFList).

0 comments on commit 4e4a9fd

Please sign in to comment.