1: -module(mongoose_instrument_metrics_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("eunit/include/eunit.hrl").
    5: -include_lib("common_test/include/ct.hrl").
    6: 
    7: -define(LABELS, #{host_type => <<"localhost">>}).
    8: -define(LABELS2, #{host_type => <<"test type">>}).
    9: -define(HOST_TYPE, <<"localhost">>).
   10: -define(HOST_TYPE2, <<"test type">>).
   11: 
   12: %% Setup and teardown
   13: 
   14: all() ->
   15:     [{group, prometheus},
   16:      {group, exometer},
   17:      {group, exometer_global},
   18:      {group, prometheus_and_exometer}
   19:     ].
   20: 
   21: groups() ->
   22:     [{prometheus, [parallel], [prometheus_skips_non_metric_event,
   23:                                prometheus_gauge_is_created_and_updated,
   24:                                prometheus_gauge_is_updated_separately_for_different_labels,
   25:                                prometheus_counter_is_created_and_updated,
   26:                                prometheus_counter_is_updated_separately_for_different_labels,
   27:                                prometheus_histogram_is_created_and_updated,
   28:                                prometheus_histogram_is_updated_separately_for_different_labels,
   29:                                multiple_prometheus_metrics_are_updated]},
   30:      {exometer, [parallel], [exometer_skips_non_metric_event,
   31:                              exometer_gauge_is_created_and_updated,
   32:                              exometer_gauge_is_updated_separately_for_different_labels,
   33:                              exometer_spiral_is_created_and_updated,
   34:                              exometer_spiral_is_updated_separately_for_different_labels,
   35:                              exometer_histogram_is_created_and_updated,
   36:                              exometer_histogram_is_updated_separately_for_different_labels,
   37:                              multiple_exometer_metrics_are_updated]},
   38:      {exometer_global, [parallel], [multiple_exometer_metrics_are_updated]},
   39:      {prometheus_and_exometer, [parallel], [prometheus_and_exometer_metrics_are_updated]}
   40:     ].
   41: 
   42: init_per_group(Group, Config) ->
   43:     [application:ensure_all_started(App) || App <- apps(Group)],
   44:     mongoose_config:set_opts(#{hosts => [?HOST_TYPE],
   45:                                host_types => [?HOST_TYPE2],
   46:                                instrumentation => opts(Group)}),
   47:     Config1 = async_helper:start(Config, mongoose_instrument, start_link, []),
   48:     mongoose_instrument:persist(),
   49:     Config1 ++ extra_config(Group).
   50: 
   51: end_per_group(_Group, Config) ->
   52:     async_helper:stop_all(Config),
   53:     mongoose_config:erase_opts().
   54: 
   55: init_per_testcase(Case, Config) ->
   56:     [{event, join_atoms(Case, event)} | Config].
   57: 
   58: end_per_testcase(_Case, _Config) ->
   59:     ok.
   60: 
   61: apps(prometheus) -> [prometheus];
   62: apps(exometer) -> [exometer_core];
   63: apps(exometer_global) -> [exometer_core];
   64: apps(prometheus_and_exometer) -> apps(prometheus) ++ apps(exometer).
   65: 
   66: opts(prometheus) -> #{prometheus => #{}};
   67: opts(exometer) -> #{exometer => #{all_metrics_are_global => false}};
   68: opts(exometer_global) -> #{exometer => #{all_metrics_are_global => true}};
   69: opts(prometheus_and_exometer) -> maps:merge(opts(prometheus), opts(exometer)).
   70: 
   71: extra_config(exometer) -> [{prefix, ?HOST_TYPE}];
   72: extra_config(exometer_global) -> [{prefix, global}];
   73: extra_config(_Group) -> [].
   74: 
   75: %% Test cases
   76: 
   77: prometheus_skips_non_metric_event(Config) ->
   78:     Event = ?config(event, Config),
   79:     false = mongoose_instrument_prometheus:set_up(Event, ?LABELS, #{}),
   80:     false = mongoose_instrument_prometheus:set_up(Event, ?LABELS, #{loglevel => error}).
   81: 
   82: prometheus_gauge_is_created_and_updated(Config) ->
   83:     Event = ?config(event, Config),
   84:     Metric = prom_name(Event, count),
   85: 
   86:     %% Prometheus gauge has no initial value, and reports the last registered value
   87:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => gauge}}),
   88:     ?assertEqual(undefined, prometheus_gauge:value(Metric, [?HOST_TYPE])),
   89:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
   90:     ?assertEqual(1, prometheus_gauge:value(Metric, [?HOST_TYPE])),
   91:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}),
   92:     ?assertEqual(2, prometheus_gauge:value(Metric, [?HOST_TYPE])).
   93: 
   94: prometheus_gauge_is_updated_separately_for_different_labels(Config) ->
   95:     Event = ?config(event, Config),
   96:     Metric = prom_name(Event, count),
   97:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => gauge}}),
   98:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => gauge}}),
   99:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  100:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}),
  101:     ?assertEqual(1, prometheus_gauge:value(Metric, [?HOST_TYPE])),
  102:     ?assertEqual(2, prometheus_gauge:value(Metric, [?HOST_TYPE2])).
  103: 
  104: prometheus_counter_is_created_and_updated(Config) ->
  105:     Event = ?config(event, Config),
  106:     Metric = prom_name(Event, count),
  107: 
  108:     %% Prometheus counter starts at zero, and reports the sum of all values
  109:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}),
  110:     ?assertEqual(0, prometheus_counter:value(Metric, [?HOST_TYPE])),
  111:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  112:     ?assertEqual(1, prometheus_counter:value(Metric, [?HOST_TYPE])),
  113:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}),
  114:     ?assertEqual(3, prometheus_counter:value(Metric, [?HOST_TYPE])).
  115: 
  116: prometheus_counter_is_updated_separately_for_different_labels(Config) ->
  117:     Event = ?config(event, Config),
  118:     Metric = prom_name(Event, count),
  119:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}),
  120:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => spiral}}),
  121:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  122:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}),
  123:     ?assertEqual(1, prometheus_counter:value(Metric, [?HOST_TYPE])),
  124:     ?assertEqual(2, prometheus_counter:value(Metric, [?HOST_TYPE2])).
  125: 
  126: prometheus_histogram_is_created_and_updated(Config) ->
  127:     Event = ?config(event, Config),
  128:     Metric = prom_name(Event, time),
  129:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}),
  130: 
  131:     %% Prometheus histogram shows no value if there is no data
  132:     ?assertEqual(undefined, prometheus_histogram:value(Metric, [?HOST_TYPE])),
  133:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}),
  134:     ?assertMatch({[1, 0|_], 1}, prometheus_histogram:value(Metric, [?HOST_TYPE])),
  135:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}),
  136:     ?assertMatch({[2, 0|_], 2}, prometheus_histogram:value(Metric, [?HOST_TYPE])),
  137:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 2}),
  138:     ?assertMatch({[2, 1|_], 4}, prometheus_histogram:value(Metric, [?HOST_TYPE])).
  139: 
  140: prometheus_histogram_is_updated_separately_for_different_labels(Config) ->
  141:     Event = ?config(event, Config),
  142:     Metric = prom_name(Event, time),
  143:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}),
  144:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{time => histogram}}),
  145:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}),
  146:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{time => 2}),
  147:     ?assertMatch({[1, 0|_], 1}, prometheus_histogram:value(Metric, [?HOST_TYPE])),
  148:     ?assertMatch({[0, 1|_], 2}, prometheus_histogram:value(Metric, [?HOST_TYPE2])).
  149: 
  150: multiple_prometheus_metrics_are_updated(Config) ->
  151:     Event = ?config(event, Config),
  152:     Counter = prom_name(Event, count),
  153:     Histogram = prom_name(Event, time),
  154:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral,
  155:                                                                    time => histogram}}),
  156:     %% Update both metrics
  157:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}),
  158:     ?assertEqual(1, prometheus_counter:value(Counter, [?HOST_TYPE])),
  159:     HistogramValue = prometheus_histogram:value(Histogram, [?HOST_TYPE]),
  160:     ?assertMatch({[0, 1|_], 2}, HistogramValue),
  161: 
  162:     %% Update only one metric
  163:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}),
  164:     ?assertEqual(3, prometheus_counter:value(Counter, [?HOST_TYPE])),
  165:     ?assertEqual(HistogramValue, prometheus_histogram:value(Histogram, [?HOST_TYPE])),
  166: 
  167:     %% No update
  168:     ok = mongoose_instrument:execute(Event, ?LABELS, #{something => irrelevant}),
  169:     ?assertEqual(3, prometheus_counter:value(Counter, [?HOST_TYPE])),
  170:     ?assertEqual(HistogramValue, prometheus_histogram:value(Histogram, [?HOST_TYPE])).
  171: 
  172: exometer_skips_non_metric_event(Config) ->
  173:     Event = ?config(event, Config),
  174:     false = mongoose_instrument_exometer:set_up(Event, ?LABELS, #{}),
  175:     false = mongoose_instrument_exometer:set_up(Event, ?LABELS, #{loglevel => error}).
  176: 
  177: exometer_gauge_is_created_and_updated(Config) ->
  178:     Event = ?config(event, Config),
  179:     Metric = [?HOST_TYPE, Event, count],
  180: 
  181:     %% Exometer gauge starts at zero, and reports the last registered value
  182:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => gauge}}),
  183:     ?assertEqual({ok, [{value, 0}]}, exometer:get_value(Metric, value)),
  184:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  185:     ?assertEqual({ok, [{value, 1}]}, exometer:get_value(Metric, value)),
  186:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}),
  187:     ?assertEqual({ok, [{value, 2}]}, exometer:get_value(Metric, value)).
  188: 
  189: exometer_gauge_is_updated_separately_for_different_labels(Config) ->
  190:     Event = ?config(event, Config),
  191:     Metric1 = [?HOST_TYPE, Event, count],
  192:     Metric2 = [<<"test_type">>, Event, count],
  193:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => gauge}}),
  194:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => gauge}}),
  195:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  196:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}),
  197:     ?assertEqual({ok, [{value, 1}]}, exometer:get_value(Metric1, value)),
  198:     ?assertEqual({ok, [{value, 2}]}, exometer:get_value(Metric2, value)).
  199: 
  200: exometer_spiral_is_created_and_updated(Config) ->
  201:     Event = ?config(event, Config),
  202:     Metric = [?HOST_TYPE, Event, count],
  203: 
  204:     %% Exometer spiral starts at zero, and reports the sum of all values
  205:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}),
  206:     ?assertEqual({ok, [{count, 0}]}, exometer:get_value(Metric, count)),
  207:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  208:     ?assertEqual({ok, [{count, 1}]}, exometer:get_value(Metric, count)),
  209:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 2}),
  210:     ?assertEqual({ok, [{count, 3}]}, exometer:get_value(Metric, count)).
  211: 
  212: exometer_spiral_is_updated_separately_for_different_labels(Config) ->
  213:     Event = ?config(event, Config),
  214:     Metric1 = [?HOST_TYPE, Event, count],
  215:     Metric2 = [<<"test_type">>, Event, count],
  216:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral}}),
  217:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{count => spiral}}),
  218:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1}),
  219:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{count => 2}),
  220:     ?assertEqual({ok, [{count, 1}]}, exometer:get_value(Metric1, count)),
  221:     ?assertEqual({ok, [{count, 2}]}, exometer:get_value(Metric2, count)).
  222: 
  223: exometer_histogram_is_created_and_updated(Config) ->
  224:     Event = ?config(event, Config),
  225:     Metric = [?HOST_TYPE, Event, time],
  226: 
  227:     %% Exometer mean value is zero if there is no data
  228:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}),
  229:     ?assertEqual({ok, [{mean, 0}]}, exometer:get_value(Metric, mean)),
  230:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}),
  231:     ?assertEqual({ok, [{mean, 1}]}, exometer:get_value(Metric, mean)),
  232:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 3}),
  233:     ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Metric, mean)).
  234: 
  235: exometer_histogram_is_updated_separately_for_different_labels(Config) ->
  236:     Event = ?config(event, Config),
  237:     Metric1 = [?HOST_TYPE, Event, time],
  238:     Metric2 = [<<"test_type">>, Event, time],
  239:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{time => histogram}}),
  240:     ok = mongoose_instrument:set_up(Event, ?LABELS2, #{metrics => #{time => histogram}}),
  241:     ok = mongoose_instrument:execute(Event, ?LABELS, #{time => 1}),
  242:     ok = mongoose_instrument:execute(Event, ?LABELS2, #{time => 3}),
  243:     ?assertEqual({ok, [{mean, 1}]}, exometer:get_value(Metric1, mean)),
  244:     ?assertEqual({ok, [{mean, 3}]}, exometer:get_value(Metric2, mean)).
  245: 
  246: multiple_exometer_metrics_are_updated(Config) ->
  247:     Event = ?config(event, Config),
  248:     Prefix = ?config(prefix, Config),
  249:     Counter = [Prefix, Event, count],
  250:     Histogram = [Prefix, Event, time],
  251:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral,
  252:                                                                    time => histogram}}),
  253:     %% Update both metrics
  254:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}),
  255:     ?assertEqual({ok, [{count, 1}]}, exometer:get_value(Counter, count)),
  256:     ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)),
  257: 
  258:     %% Update only one metric
  259:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 5}),
  260:     ?assertEqual({ok, [{count, 6}]}, exometer:get_value(Counter, count)),
  261:     ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)),
  262: 
  263:     %% No update
  264:     ok = mongoose_instrument:execute(Event, ?LABELS, #{something => irrelevant}),
  265:     ?assertEqual({ok, [{count, 6}]}, exometer:get_value(Counter, count)),
  266:     ?assertEqual({ok, [{mean, 2}]}, exometer:get_value(Histogram, mean)).
  267: 
  268: prometheus_and_exometer_metrics_are_updated(Config) ->
  269:     Event = ?config(event, Config),
  270:     ok = mongoose_instrument:set_up(Event, ?LABELS, #{metrics => #{count => spiral,
  271:                                                                    time => histogram}}),
  272:     ok = mongoose_instrument:execute(Event, ?LABELS, #{count => 1, time => 2}),
  273:     ?assertEqual({ok, [{count, 1}]}, exometer:get_value([?HOST_TYPE, Event, count], count)),
  274:     ?assertEqual({ok, [{mean, 2}]}, exometer:get_value([?HOST_TYPE, Event, time], mean)),
  275:     ?assertEqual(1, prometheus_counter:value(prom_name(Event, count), [?HOST_TYPE])),
  276:     ?assertMatch({[0, 1|_], 2}, prometheus_histogram:value(prom_name(Event, time), [?HOST_TYPE])).
  277: 
  278: %% Helpers
  279: 
  280: join_atoms(A1, A2) ->
  281:     list_to_atom(join_atoms_to_list(A1, A2)).
  282: 
  283: prom_name(EventName, MetricName) ->
  284:     join_atoms_to_list(EventName, MetricName).
  285: 
  286: join_atoms_to_list(A1, A2) ->
  287:     atom_to_list(A1) ++ "_" ++ atom_to_list(A2).