diff --git a/src/metrics/mongoose_instrument.erl b/src/metrics/mongoose_instrument.erl index 67d2e22ce53..cc4a32ad92a 100644 --- a/src/metrics/mongoose_instrument.erl +++ b/src/metrics/mongoose_instrument.erl @@ -36,7 +36,12 @@ set_up(EventName, Labels, Config) -> AllModules = handler_modules(), UsedModules = lists:filter(fun(Mod) -> Mod:set_up(EventName, Labels, Config) end, AllModules), HandlerFuns = [fun Mod:handle_event/4 || Mod <- UsedModules], - mongoose_instrument_registry:attach(EventName, Labels, {HandlerFuns, Config}). + case mongoose_instrument_registry:attach(EventName, Labels, {HandlerFuns, Config}) of + ok -> + ok; + {error, already_attached} -> + error(#{what => event_already_registered, event_name => EventName, labels => Labels}) + end. -spec tear_down(event_name(), labels()) -> ok. tear_down(EventName, Labels) -> @@ -48,16 +53,25 @@ span(Event, Labels, F, MeasureF) -> -spec span(event_name(), labels(), fun((...) -> Result), list(), measure_fun(Result)) -> Result. span(Event, Labels, F, Args, MeasureF) -> - {ok, Handlers} = mongoose_instrument_registry:lookup(Event, Labels), + Handlers = get_handlers(Event, Labels), {Time, Result} = timer:tc(F, Args), handle_event(Event, Labels, MeasureF(Time, Result), Handlers), Result. -spec execute(event_name(), labels(), measurements()) -> ok. execute(Event, Labels, Measurements) -> - {ok, Handlers} = mongoose_instrument_registry:lookup(Event, Labels), + Handlers = get_handlers(Event, Labels), handle_event(Event, Labels, Measurements, Handlers). +-spec get_handlers(event_name(), labels()) -> handlers(). +get_handlers(EventName, Labels) -> + case mongoose_instrument_registry:lookup(EventName, Labels) of + {ok, Handlers} -> + Handlers; + {error, not_found} -> + error(#{what => event_not_registered, event_name => EventName, labels => Labels}) + end. + -spec handle_event(event_name(), labels(), measurements(), handlers()) -> ok. handle_event(Event, Labels, Measurements, {EventHandlers, Config}) -> lists:foreach(fun(Handler) -> Handler(Event, Labels, Config, Measurements) end, EventHandlers). diff --git a/src/metrics/mongoose_instrument_exometer.erl b/src/metrics/mongoose_instrument_exometer.erl index 2ae22208bcf..986cc8fb169 100644 --- a/src/metrics/mongoose_instrument_exometer.erl +++ b/src/metrics/mongoose_instrument_exometer.erl @@ -5,7 +5,7 @@ -export([set_up/3, handle_event/4]). -spec set_up(mongoose_instrument:event_name(), mongoose_instrument:labels(), - mongoose_instrument:config()) -> boolean(). + mongoose_instrument:config()) -> boolean(). set_up(EventName, Labels, #{metrics := Metrics}) -> maps:foreach(fun(MetricName, MetricType) -> set_up_metric(EventName, Labels, MetricName, MetricType) diff --git a/src/metrics/mongoose_instrument_registry.erl b/src/metrics/mongoose_instrument_registry.erl index c2217f12cf6..a6b46114e38 100644 --- a/src/metrics/mongoose_instrument_registry.erl +++ b/src/metrics/mongoose_instrument_registry.erl @@ -8,10 +8,12 @@ start() -> ok. -spec attach(mongoose_instrument:event_name(), mongoose_instrument:labels(), - mongoose_instrument:handlers()) -> ok. + mongoose_instrument:handlers()) -> ok | {error, already_attached}. attach(Event, Labels, Val) -> - ets:insert_new(?MODULE, {{Event, Labels}, Val}), - ok. + case ets:insert_new(?MODULE, {{Event, Labels}, Val}) of + true -> ok; + false -> {error, already_attached} + end. -spec detach(mongoose_instrument:event_name(), mongoose_instrument:labels()) -> ok. detach(Event, Labels) -> diff --git a/test/mongoose_instrument_SUITE.erl b/test/mongoose_instrument_SUITE.erl new file mode 100644 index 00000000000..46f11bbfc2e --- /dev/null +++ b/test/mongoose_instrument_SUITE.erl @@ -0,0 +1,167 @@ +-module(mongoose_instrument_SUITE). +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(HANDLER, mongoose_instrument_test_handler). +-define(INACTIVE_HANDLER, mongoose_instrument_inactive_handler). +-define(LABELS, #{key => value}). +-define(CFG, #{metrics => #{time => histogram}}). +-define(MEASUREMENTS, #{count => 1}). + +%% Setup and teardown + +all() -> + [{group, api}]. + +groups() -> + [{api, [parallel], [set_up_and_execute, + set_up_multiple_and_execute_one, + set_up_crashes_when_repeated, + set_up_and_tear_down, + set_up_and_tear_down_multiple, + execute_crashes_when_not_set_up, + set_up_and_span, + set_up_and_span_with_arg, + set_up_and_span_with_error, + span_crashes_when_not_set_up]} + ]. + +init_per_suite(Config) -> + mongoose_config:set_opts(opts()), + mock_handler(?HANDLER, true), + mock_handler(?INACTIVE_HANDLER, false), + async_helper:start(Config, mongoose_instrument_registry, start, []). + +mock_handler(Module, SetUpResult) -> + meck:new(Module, [non_strict, no_link]), + meck:expect(Module, set_up, fun(_Event, #{}, #{}) -> SetUpResult end), + meck:expect(Module, handle_event, fun(_Event, #{}, #{}, #{}) -> ok end). + +end_per_suite(Config) -> + async_helper:stop_all(Config), + meck:unload(?HANDLER), + meck:unload(?INACTIVE_HANDLER), + mongoose_config:erase_opts(). + +init_per_testcase(Case, Config) -> + [{event, event_name(Case)} | Config]. + +end_per_testcase(_Case, _Config) -> + ok. + +opts() -> + #{instrumentation => #{test_handler => #{}, inactive_handler => #{}}}. + +event_name(Case) -> + list_to_atom(atom_to_list(Case) ++ "_event"). + +%% Test cases + +set_up_and_execute(Config) -> + Event = ?config(event, Config), + ok = mongoose_instrument:set_up(Event, ?LABELS, ?CFG), + ?assertEqual([{Event, ?LABELS, ?CFG, true}], history(?HANDLER, set_up)), + ?assertEqual([{Event, ?LABELS, ?CFG, false}], history(?INACTIVE_HANDLER, set_up)), + ok = mongoose_instrument:execute(Event, ?LABELS, ?MEASUREMENTS), + ?assertEqual([{Event, ?LABELS, ?CFG, ?MEASUREMENTS}], history(?HANDLER, handle_event)), + ?assertEqual([], history(?INACTIVE_HANDLER, handle_event)). + +set_up_multiple_and_execute_one(Config) -> + Event = ?config(event, Config), + Specs = [{Event, Labels1 = #{key1 => value1}, ?CFG}, + {Event, Labels2 = #{key2 => value2}, ?CFG}], + ok = mongoose_instrument:set_up(Specs), + ?assertEqual([{Event, Labels1, ?CFG, true}, + {Event, Labels2, ?CFG, true}], history(?HANDLER, set_up)), + ok = mongoose_instrument:execute(Event, Labels1, ?MEASUREMENTS), + ?assertEqual([{Event, Labels1, ?CFG, ?MEASUREMENTS}], history(?HANDLER, handle_event)). + +set_up_crashes_when_repeated(Config) -> + Event = ?config(event, Config), + ok = mongoose_instrument:set_up(Event, ?LABELS, ?CFG), + ?assertError(#{what := event_already_registered}, + mongoose_instrument:set_up(Event, ?LABELS, ?CFG)). + +set_up_and_tear_down(Config) -> + Event = ?config(event, Config), + ok = mongoose_instrument:set_up(Event, ?LABELS, ?CFG), + ok = mongoose_instrument:tear_down(Event, ?LABELS), + ?assertError(#{what := event_not_registered}, + mongoose_instrument:execute(Event, ?LABELS, ?MEASUREMENTS)), + [] = history(?HANDLER, handle_event), + ok = mongoose_instrument:tear_down(Event, ?LABELS). % idempotent + +set_up_and_tear_down_multiple(Config) -> + Event = ?config(event, Config), + Specs = [{Event, Labels1 = #{key1 => value1}, ?CFG}, + {Event, Labels2 = #{key2 => value2}, ?CFG}], + ok = mongoose_instrument:set_up(Specs), + ok = mongoose_instrument:tear_down(Specs), + ?assertError(#{what := event_not_registered}, + mongoose_instrument:execute(Event, Labels1, ?MEASUREMENTS)), + ?assertError(#{what := event_not_registered}, + mongoose_instrument:execute(Event, Labels2, ?MEASUREMENTS)), + [] = history(?HANDLER, handle_event). + +execute_crashes_when_not_set_up(Config) -> + Event = ?config(event, Config), + ?assertError(#{what := event_not_registered}, + mongoose_instrument:execute(Event, ?LABELS, ?MEASUREMENTS)), + [] = history(?HANDLER, handle_event). + +set_up_and_span(Config) -> + {Event, Labels, InstrConfig} = {?config(event, Config), ?LABELS, ?CFG}, + ok = mongoose_instrument:set_up(Event, Labels, InstrConfig), + ok = mongoose_instrument:span(Event, Labels, fun test_op/0, fun measure_test_op/2), + [{Event, Labels, InstrConfig, #{time := Time, result := ok}}] = history(?HANDLER, handle_event), + ?assert(Time > 1000). + +set_up_and_span_with_arg(Config) -> + {Event, Labels, InstrConfig} = {?config(event, Config), ?LABELS, ?CFG}, + ok = mongoose_instrument:set_up(Event, Labels, InstrConfig), + ok = mongoose_instrument:span(Event, Labels, fun test_op/1, [2], fun measure_test_op/2), + [{Event, Labels, InstrConfig, #{time := Time, result := ok}}] = history(?HANDLER, handle_event), + ?assert(Time > 2000). + +set_up_and_span_with_error(Config) -> + Event = ?config(event, Config), + ok = mongoose_instrument:set_up(Event, ?LABELS, ?CFG), + ?assertError(simulated_error, + mongoose_instrument:span(Event, ?LABELS, fun crashing_op/0, fun measure_test_op/2)), + [] = history(?HANDLER, handle_event). + +span_crashes_when_not_set_up(Config) -> + Event = ?config(event, Config), + Labels = #{key => value}, + ?assertError(#{what := event_not_registered}, + mongoose_instrument:span(Event, Labels, fun test_op/0, fun measure_test_op/2)), + %% Also checks that the function is not executed - otherwise 'simulated_error' would be raised + ?assertError(#{what := event_not_registered}, + mongoose_instrument:span(Event, Labels, fun crashing_op/0, fun measure_test_op/2)), + [] = history(?HANDLER, handle_event). + +%% Helpers + +history(HandlerMod, WantedF) -> + [process_history(CalledF, Args, Result) || + {_Pid, {_M, CalledF, Args}, Result} <- meck:history(HandlerMod, self()), + WantedF =:= CalledF]. + +process_history(set_up, [Event, Labels, InstrConfig], Result) -> + {Event, Labels, InstrConfig, Result}; +process_history(handle_event, [Event, Labels, InstrConfig, Measurements], ok) -> + {Event, Labels, InstrConfig, Measurements}. + +test_op(Delay) -> + timer:sleep(Delay). + +test_op() -> + test_op(1). + +crashing_op() -> + error(simulated_error). + +measure_test_op(Time, Result) -> + #{time => Time, result => Result}. diff --git a/test/mongoose_instrument_metrics_SUITE.erl b/test/mongoose_instrument_metrics_SUITE.erl new file mode 100644 index 00000000000..e4098de6318 --- /dev/null +++ b/test/mongoose_instrument_metrics_SUITE.erl @@ -0,0 +1,198 @@ +-module(mongoose_instrument_metrics_SUITE). +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(LABELS, #{host_type => <<"localhost">>}). +-define(LABELS2, #{host_type => <<"test type">>}). +-define(HOST_TYPE, <<"localhost">>). +-define(HOST_TYPE2, <<"test type">>). + +%% Setup and teardown + +all() -> + [{group, prometheus}, + {group, exometer}, + {group, prometheus_and_exometer} + ]. + +groups() -> + [{prometheus, [parallel], [prometheus_skips_non_metric_event, + prometheus_counter_is_created_but_not_initialized, + prometheus_counter_is_updated_separately_for_different_labels, + prometheus_histogram_is_created_but_not_initialized, + prometheus_histogram_is_updated_separately_for_different_labels, + multiple_prometheus_metrics_are_updated]}, + {exometer, [parallel], [exometer_skips_non_metric_event, + exometer_spiral_is_created_and_initialized, + exometer_spiral_is_updated_separately_for_different_labels, + exometer_histogram_is_created_and_initialized, + exometer_histogram_is_updated_separately_for_different_labels, + multiple_exometer_metrics_are_updated]}, + {prometheus_and_exometer, [parallel], [prometheus_and_exometer_metrics_are_updated]} + ]. + +init_per_suite(Config) -> + async_helper:start(Config, mongoose_instrument_registry, start, []). + +end_per_suite(Config) -> + async_helper:stop_all(Config). + +init_per_group(Group, Config) -> + [application:ensure_all_started(App) || App <- apps(Group)], + mongoose_config:set_opts(#{instrumentation => opts(Group)}), + Config. + +end_per_group(_Group, _Config) -> + mongoose_config:erase_opts(). + +init_per_testcase(Case, Config) -> + [{event, concat(Case, event)} | Config]. + +end_per_testcase(_Case, _Config) -> + ok. + +apps(prometheus) -> [prometheus]; +apps(exometer) -> [exometer_core]; +apps(prometheus_and_exometer) -> apps(prometheus) ++ apps(exometer). + +opts(prometheus) -> #{prometheus => #{}}; +opts(exometer) -> #{exometer => #{}}; +opts(prometheus_and_exometer) -> maps:merge(opts(prometheus), opts(exometer)). + +%% Test cases + +prometheus_skips_non_metric_event(Config) -> + Event = ?config(event, Config), + false = mongoose_instrument_prometheus:set_up(Event, ?LABELS, #{}), + false = mongoose_instrument_prometheus:set_up(Event, ?LABELS, #{loglevel => error}). + +prometheus_counter_is_created_but_not_initialized(Config) -> + Event = ?config(event, Config), + Metric = concat(Event, count), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}), + ?assertEqual(undefined, prometheus_counter:value(Metric, [?HOST_TYPE])). + +prometheus_counter_is_updated_separately_for_different_labels(Config) -> + Event = ?config(event, Config), + Metric = concat(Event, count), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}), + ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => spiral}}), + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}), + ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}), + ?assertEqual(1, prometheus_counter:value(Metric, [?HOST_TYPE])), + ?assertEqual(2, prometheus_counter:value(Metric, [?HOST_TYPE2])). + +prometheus_histogram_is_created_but_not_initialized(Config) -> + Event = ?config(event, Config), + Metric = concat(Event, time), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}), + ?assertEqual(undefined, prometheus_histogram:value(Metric, [?HOST_TYPE])). + +prometheus_histogram_is_updated_separately_for_different_labels(Config) -> + Event = ?config(event, Config), + Metric = concat(Event, time), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}), + ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{time => histogram}}), + ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}), + ok = mongoose_instrument:execute(Event, ?LABELS2, #{time => 2}), + ?assertMatch({[1, 0|_], 1}, prometheus_histogram:value(Metric, [?HOST_TYPE])), + ?assertMatch({[0, 1|_], 2}, prometheus_histogram:value(Metric, [?HOST_TYPE2])). + +multiple_prometheus_metrics_are_updated(Config) -> + Event = ?config(event, Config), + Counter = concat(Event, count), + Histogram = concat(Event, time), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral, + time => histogram}}), + %% Update both metrics + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}), + ?assertEqual(1, prometheus_counter:value(Counter, [?HOST_TYPE])), + HistogramValue = prometheus_histogram:value(Histogram, [?HOST_TYPE]), + ?assertMatch({[0, 1|_], 2}, HistogramValue), + + %% Update only one metric + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}), + ?assertEqual(3, prometheus_counter:value(Counter, [?HOST_TYPE])), + ?assertEqual(HistogramValue, prometheus_histogram:value(Histogram, [?HOST_TYPE])), + + %% No update + ok = mongoose_instrument:execute(Event, ?LABELS, #{something => irrelevant}), + ?assertEqual(3, prometheus_counter:value(Counter, [?HOST_TYPE])), + ?assertEqual(HistogramValue, prometheus_histogram:value(Histogram, [?HOST_TYPE])). + +exometer_skips_non_metric_event(Config) -> + Event = ?config(event, Config), + false = mongoose_instrument_exometer:set_up(Event, ?LABELS, #{}), + false = mongoose_instrument_exometer:set_up(Event, ?LABELS, #{loglevel => error}). + +exometer_spiral_is_created_and_initialized(Config) -> + Event = ?config(event, Config), + Metric = [?HOST_TYPE, Event, count], + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}), + ?assertEqual({ok, [{count, 0}]}, exometer:get_value(Metric, count)). + +exometer_spiral_is_updated_separately_for_different_labels(Config) -> + Event = ?config(event, Config), + Metric1 = [?HOST_TYPE, Event, count], + Metric2 = [<<"test_type">>, Event, count], + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}), + ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => spiral}}), + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}), + ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}), + ?assertEqual({ok, [{count, 1}]}, exometer:get_value(Metric1, count)), + ?assertEqual({ok, [{count, 2}]}, exometer:get_value(Metric2, count)). + +exometer_histogram_is_created_and_initialized(Config) -> + Event = ?config(event, Config), + Metric = [?HOST_TYPE, Event, time], + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}), + ?assertEqual({ok, [{mean, 0}]}, exometer:get_value(Metric, mean)). + +exometer_histogram_is_updated_separately_for_different_labels(Config) -> + Event = ?config(event, Config), + Metric1 = [?HOST_TYPE, Event, time], + Metric2 = [<<"test_type">>, Event, time], + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}), + ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{time => histogram}}), + ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}), + ok = mongoose_instrument:execute(Event, ?LABELS2, #{time => 3}), + ?assertEqual({ok, [{mean, 1}]}, exometer:get_value(Metric1, mean)), + ?assertEqual({ok, [{mean, 3}]}, exometer:get_value(Metric2, mean)). + +multiple_exometer_metrics_are_updated(Config) -> + Event = ?config(event, Config), + Counter = [?HOST_TYPE, Event, count], + Histogram = [?HOST_TYPE, Event, time], + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral, + time => histogram}}), + %% Update both metrics + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}), + ?assertEqual({ok, [{count, 1}]}, exometer:get_value(Counter, count)), + ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)), + + %% Update only one metric + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 5}), + ?assertEqual({ok, [{count, 6}]}, exometer:get_value(Counter, count)), + ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)), + + %% No update + ok = mongoose_instrument:execute(Event, ?LABELS, #{something => irrelevant}), + ?assertEqual({ok, [{count, 6}]}, exometer:get_value(Counter, count)), + ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)). + +prometheus_and_exometer_metrics_are_updated(Config) -> + Event = ?config(event, Config), + ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral, + time => histogram}}), + ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}), + ?assertEqual({ok, [{count, 1}]}, exometer:get_value([?HOST_TYPE, Event, count], count)), + ?assertEqual({ok, [{mean, 2}]}, exometer:get_value([?HOST_TYPE, Event, time], mean)), + ?assertEqual(1, prometheus_counter:value(concat(Event, count), [?HOST_TYPE])), + ?assertMatch({[0, 1|_], 2}, prometheus_histogram:value(concat(Event, time), [?HOST_TYPE])). + +%% Helpers + +concat(A1, A2) -> + list_to_atom(atom_to_list(A1) ++ "_" ++ atom_to_list(A2)).