1: -module(service_mongoose_system_metrics_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include_lib("common_test/include/ct.hrl").
    6: -include_lib("eunit/include/eunit.hrl").
    7: 
    8: -define(SERVER_URL, "http://localhost:8765").
    9: -define(ETS_TABLE, qs).
   10: -define(TRACKING_ID, #{id => "G-7KQE4W9SVJ", secret => "Secret"}).
   11: -define(TRACKING_ID_CI, #{id => "G-VB91V60SKT", secret => "Secret2"}).
   12: -define(TRACKING_ID_EXTRA, #{id => "UA-EXTRA-TRACKING-ID", secret => "Secret3"}).
   13: 
   14: -record(event, {
   15:     name = <<>>,
   16:     params = #{},
   17:     client_id = <<>>,
   18:     instance_id = <<>>,
   19:     app_secret = <<>>}).
   20: 
   21: -import(distributed_helper, [mim/0, mim2/0, mim3/0, rpc/4,
   22:                              require_rpc_nodes/1
   23:                             ]).
   24: 
   25: -import(component_helper, [connect_component/1,
   26:                            disconnect_component/2,
   27:                            get_components/1]).
   28: 
   29: -import(domain_helper, [host_type/0]).
   30: -import(config_parser_helper, [mod_config/2, config/2]).
   31: 
   32: suite() ->
   33:     require_rpc_nodes([mim]).
   34: 
   35: all() ->
   36:     [
   37:      system_metrics_are_not_reported_when_not_allowed,
   38:      periodic_report_available,
   39:      all_clustered_mongooses_report_the_same_client_id,
   40:      system_metrics_are_reported_to_google_analytics_when_mim_starts,
   41:      system_metrics_are_reported_to_configurable_google_analytics,
   42:      system_metrics_are_reported_to_a_json_file,
   43:      mongoose_version_is_reported,
   44:      cluster_uptime_is_reported,
   45:      xmpp_components_are_reported,
   46:      api_are_reported,
   47:      transport_mechanisms_are_reported,
   48:      outgoing_pools_are_reported,
   49:      xmpp_stanzas_counts_are_reported,
   50:      config_type_is_reported,
   51:      {group, module_opts},
   52:      {group, log_transparency}
   53:     ].
   54: 
   55: groups() ->
   56:     [
   57:      {module_opts, [], [
   58:                         module_opts_are_reported,
   59:                         rdbms_module_opts_are_reported
   60:                        ]},
   61:      {log_transparency, [], [
   62:                              just_removed_from_config_logs_question,
   63:                              in_config_unmodified_logs_request_for_agreement,
   64:                              in_config_with_explicit_no_report_goes_off_silently,
   65:                              in_config_with_explicit_reporting_goes_on_silently
   66:                             ]}
   67:     ].
   68: 
   69: -define(APPS, [inets, crypto, ssl, ranch, cowlib, cowboy]).
   70: 
   71: %%--------------------------------------------------------------------
   72: %% Suite configuration
   73: %%--------------------------------------------------------------------
   74: init_per_suite(Config) ->
   75:     [ {ok, _} = application:ensure_all_started(App) || App <- ?APPS ],
   76:     http_helper:start(8765, "/[...]", fun handler_init/1),
   77:     Config1 = escalus:init_per_suite(Config),
   78:     Config2 = dynamic_services:save_services([mim(), mim2()], Config1),
   79:     ejabberd_node_utils:init(Config2).
   80: 
   81: end_per_suite(Config) ->
   82:     http_helper:stop(),
   83:     dynamic_services:restore_services(Config),
   84:     escalus:end_per_suite(Config).
   85: 
   86: %%--------------------------------------------------------------------
   87: %% Init & teardown
   88: %%--------------------------------------------------------------------
   89: init_per_group(module_opts, Config) ->
   90:     dynamic_modules:save_modules(host_type(), Config);
   91: init_per_group(log_transparency, Config) ->
   92:     logger_ct_backend:start(),
   93:     logger_ct_backend:capture(warning),
   94:     Config;
   95: init_per_group(_GroupName, Config) ->
   96:     Config.
   97: 
   98: end_per_group(module_opts, Config) ->
   99:     dynamic_modules:restore_modules(Config);
  100: end_per_group(log_transparency, Config) ->
  101:     logger_ct_backend:stop_capture(),
  102:     Config;
  103: end_per_group(_GroupName, Config) ->
  104:     Config.
  105: 
  106: init_per_testcase(system_metrics_are_not_reported_when_not_allowed, Config) ->
  107:     create_events_collection(),
  108:     disable_system_metrics(mim()),
  109:     delete_prev_client_id(mim()),
  110:     Config;
  111: init_per_testcase(all_clustered_mongooses_report_the_same_client_id, Config) ->
  112:     create_events_collection(),
  113:     distributed_helper:add_node_to_cluster(mim2(), Config),
  114:     enable_system_metrics(mim()),
  115:     enable_system_metrics(mim2()),
  116:     Config;
  117: init_per_testcase(system_metrics_are_reported_to_configurable_google_analytics, Config) ->
  118:     create_events_collection(),
  119:     enable_system_metrics_with_configurable_tracking_id(mim()),
  120:     Config;
  121: init_per_testcase(xmpp_components_are_reported, Config) ->
  122:     create_events_collection(),
  123:     Config1 = get_components(Config),
  124:     enable_system_metrics(mim()),
  125:     Config1;
  126: init_per_testcase(xmpp_stanzas_counts_are_reported = CN, Config) ->
  127:     create_events_collection(),
  128:     enable_system_metrics(mim()),
  129:     Config1 = escalus:create_users(Config, escalus:get_users([alice, bob])),
  130:     escalus:init_per_testcase(CN, Config1);
  131: init_per_testcase(rdbms_module_opts_are_reported = CN, Config) ->
  132:     case mongoose_helper:is_rdbms_enabled(host_type()) of
  133:         false ->
  134:             {skip, "RDBMS is not available"};
  135:         true ->
  136:             create_events_collection(),
  137:             dynamic_modules:ensure_modules(host_type(), required_modules(CN)),
  138:             enable_system_metrics(mim()),
  139:             Config
  140:     end;
  141: init_per_testcase(module_opts_are_reported = CN, Config) ->
  142:     create_events_collection(),
  143:     dynamic_modules:ensure_modules(host_type(), required_modules(CN)),
  144:     enable_system_metrics(mim()),
  145:     Config;
  146: init_per_testcase(_TestcaseName, Config) ->
  147:     create_events_collection(),
  148:     enable_system_metrics(mim()),
  149:     Config.
  150: 
  151: end_per_testcase(system_metrics_are_not_reported_when_not_allowed, Config) ->
  152:     clear_events_collection(),
  153:     delete_prev_client_id(mim()),
  154:     Config;
  155: end_per_testcase(all_clustered_mongooses_report_the_same_client_id , Config) ->
  156:     clear_events_collection(),
  157:     delete_prev_client_id(mim()),
  158:     Nodes = [mim(), mim2()],
  159:     [ begin delete_prev_client_id(Node), disable_system_metrics(Node) end || Node <- Nodes ],
  160:     distributed_helper:remove_node_from_cluster(mim2(), Config),
  161:     Config;
  162: end_per_testcase(xmpp_stanzas_counts_are_reported = CN, Config) ->
  163:     clear_events_collection(),
  164:     disable_system_metrics(mim()),
  165:     escalus:delete_users(Config, escalus:get_users([alice, bob])),
  166:     escalus:end_per_testcase(CN, Config);
  167: end_per_testcase(_TestcaseName, Config) ->
  168:     clear_events_collection(),
  169:     disable_system_metrics(mim()),
  170:     delete_prev_client_id(mim()),
  171:     Config.
  172: 
  173: 
  174: %%--------------------------------------------------------------------
  175: %% Tests
  176: %%--------------------------------------------------------------------
  177: system_metrics_are_not_reported_when_not_allowed(_Config) ->
  178:     true = system_metrics_service_is_disabled(mim()).
  179: 
  180: periodic_report_available(_Config) ->
  181:     ReportsNumber = get_events_collection_size(),
  182:     mongoose_helper:wait_until(
  183:         fun() ->
  184:                 NewReportsNumber = get_events_collection_size(),
  185:                 NewReportsNumber > ReportsNumber + 1
  186:         end,
  187:         true).
  188: 
  189: all_clustered_mongooses_report_the_same_client_id(_Config) ->
  190:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  191:     all_event_have_the_same_client_id().
  192: 
  193: system_metrics_are_reported_to_google_analytics_when_mim_starts(_Config) ->
  194:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  195:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  196:     mongoose_helper:wait_until(fun events_are_reported_to_primary_tracking_id/0, true),
  197:     all_event_have_the_same_client_id().
  198: 
  199: system_metrics_are_reported_to_configurable_google_analytics(_Config) ->
  200:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  201:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  202:     mongoose_helper:wait_until(fun events_are_reported_to_both_tracking_ids/0, true),
  203:     all_event_have_the_same_client_id().
  204: 
  205: system_metrics_are_reported_to_a_json_file(_Config) ->
  206:     ReportFilePath = rpc(mim(), mongoose_system_metrics_file, location, []),
  207:     ReportLastModified = rpc(mim(), filelib, last_modified, [ReportFilePath]),
  208:     Fun = fun() ->
  209:         ReportLastModified < rpc(mim(), filelib, last_modified, [ReportFilePath])
  210:     end,
  211:     mongoose_helper:wait_until(Fun, true),
  212:     %% now we read the content of the file and check if it's a valid JSON
  213:     {ok, File} = rpc(mim(), file, read_file, [ReportFilePath]),
  214:     jiffy:decode(File).
  215: 
  216: module_opts_are_reported(_Config) ->
  217:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  218:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  219:     check_module_backend(mod_bosh, mnesia),
  220:     check_module_backend(mod_event_pusher, push),
  221:     check_module_backend(mod_event_pusher_push, Backend),
  222:     check_module_backend(mod_http_upload, s3),
  223:     check_module_backend(mod_last, Backend),
  224:     check_module_backend(mod_muc, Backend),
  225:     check_module_backend(mod_muc_light, Backend),
  226:     check_module_backend(mod_offline, Backend),
  227:     check_module_backend(mod_privacy, Backend),
  228:     check_module_backend(mod_private, Backend),
  229:     check_module_backend(mod_pubsub, Backend),
  230:     check_module_opt(mod_push_service_mongoosepush, <<"api_version">>, <<"v3">>),
  231:     check_module_backend(mod_roster, Backend),
  232:     check_module_backend(mod_vcard, Backend).
  233: 
  234: rdbms_module_opts_are_reported(_Config) ->
  235:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  236:     check_module_backend(mod_auth_token, rdbms),
  237:     check_module_backend(mod_inbox, rdbms),
  238:     check_module_backend(mod_mam, rdbms).
  239: 
  240: check_module_backend(Module, Backend) ->
  241:     check_module_opt(Module, <<"backend">>, atom_to_binary(Backend)).
  242: 
  243: mongoose_version_is_reported(_Config) ->
  244:     mongoose_helper:wait_until(fun is_mongoose_version_reported/0, true).
  245: 
  246: cluster_uptime_is_reported(_Config) ->
  247:     mongoose_helper:wait_until(fun is_cluster_uptime_reported/0, true).
  248: 
  249: xmpp_components_are_reported(Config) ->
  250:     CompOpts = ?config(component1, Config),
  251:     {Component, Addr, _} = connect_component(CompOpts),
  252:     mongoose_helper:wait_until(fun are_xmpp_components_reported/0, true),
  253:     mongoose_helper:wait_until(fun more_than_one_component_is_reported/0, true),
  254:     disconnect_component(Component, Addr).
  255: 
  256: api_are_reported(_Config) ->
  257:     mongoose_helper:wait_until(fun is_api_reported/0, true).
  258: 
  259: transport_mechanisms_are_reported(_Config) ->
  260:     mongoose_helper:wait_until(fun are_transport_mechanisms_reported/0, true).
  261: 
  262: outgoing_pools_are_reported(_Config) ->
  263:     mongoose_helper:wait_until(fun are_outgoing_pools_reported/0, true).
  264: 
  265: xmpp_stanzas_counts_are_reported(Config) ->
  266:     escalus:story(Config, [{alice,1}, {bob,1}], fun(Alice, Bob) ->
  267:         mongoose_helper:wait_until(fun is_message_count_reported/0, true),
  268:         mongoose_helper:wait_until(fun is_iq_count_reported/0, true),
  269:         Sent = get_metric_value(<<"xmppMessageSent">>),
  270:         Received = get_metric_value(<<"xmppMessageReceived">>),
  271:         escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"Hi">>)),
  272:         escalus:assert(is_chat_message, [<<"Hi">>], escalus:wait_for_stanza(Bob)),
  273:         F = fun() -> assert_message_count_is_incremented(Sent, Received) end,
  274:         mongoose_helper:wait_until(F, ok, #{sleep_time => 500, time_left => timer:seconds(20)})
  275:     end).
  276: 
  277: config_type_is_reported(_Config) ->
  278:     mongoose_helper:wait_until(fun is_config_type_reported/0, true).
  279: 
  280: just_removed_from_config_logs_question(_Config) ->
  281:     disable_system_metrics(mim3()),
  282:     %% WHEN
  283:     Result = distributed_helper:rpc(
  284:                mim3(), service_mongoose_system_metrics, verify_if_configured, []),
  285:     %% THEN
  286:     ?assertEqual(ignore, Result).
  287: 
  288: in_config_unmodified_logs_request_for_agreement(_Config) ->
  289:     %% WHEN
  290:     disable_system_metrics(mim()),
  291:     logger_ct_backend:capture(warning),
  292:     enable_system_metrics(mim()),
  293:     %% THEN
  294:     FilterFun = fun(_, Msg) ->
  295:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch
  296:                 end,
  297:     mongoose_helper:wait_until(fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1),
  298:     %% CLEAN
  299:     logger_ct_backend:stop_capture(),
  300:     disable_system_metrics(mim()).
  301: 
  302: in_config_with_explicit_no_report_goes_off_silently(_Config) ->
  303:     %% WHEN
  304:     logger_ct_backend:capture(warning),
  305:     start_system_metrics_service(mim(), #{report => false}),
  306:     logger_ct_backend:stop_capture(),
  307:     %% THEN
  308:     FilterFun = fun(warning, Msg) ->
  309:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch;
  310:                    (_,_) -> false
  311:                 end,
  312:     [] = logger_ct_backend:recv(FilterFun),
  313:     %% CLEAN
  314:     disable_system_metrics(mim()).
  315: 
  316: in_config_with_explicit_reporting_goes_on_silently(_Config) ->
  317:     %% WHEN
  318:     logger_ct_backend:capture(warning),
  319:     start_system_metrics_service(mim(), #{report => true}),
  320:     logger_ct_backend:stop_capture(),
  321:     %% THEN
  322:     FilterFun = fun(warning, Msg) ->
  323:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch;
  324:                    (_,_) -> false
  325:                 end,
  326:     [] = logger_ct_backend:recv(FilterFun),
  327:     %% CLEAN
  328:     disable_system_metrics(mim()).
  329: 
  330: %%--------------------------------------------------------------------
  331: %% Helpers
  332: %%--------------------------------------------------------------------
  333: 
  334: required_modules(CaseName) ->
  335:     lists:filter(fun({Module, _Opts}) -> is_module_supported(Module) end,
  336:                  modules_to_test(CaseName)).
  337: 
  338: modules_to_test(module_opts_are_reported) ->
  339:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  340:     [required_module(mod_bosh),
  341:      required_module(mod_event_pusher,
  342:                      #{push => config([modules, mod_event_pusher, push], #{backend => Backend})}),
  343:      required_module(mod_http_upload, s3),
  344:      required_module(mod_last, Backend),
  345:      required_module(mod_muc, Backend),
  346:      required_module(mod_muc_light, Backend),
  347:      required_module(mod_offline, Backend),
  348:      required_module(mod_privacy, Backend),
  349:      required_module(mod_private, Backend),
  350:      required_module(mod_pubsub, Backend),
  351:      required_module(mod_push_service_mongoosepush),
  352:      required_module(mod_roster, Backend),
  353:      required_module(mod_vcard, Backend)];
  354: modules_to_test(rdbms_module_opts_are_reported) ->
  355:     [required_module(mod_auth_token),
  356:      required_module(mod_inbox),
  357:      required_module(mod_mam)].
  358: 
  359: required_module(Module) ->
  360:     required_module(Module, #{}).
  361: 
  362: required_module(Module, Backend) when is_atom(Backend) ->
  363:     {Module, mod_config(Module, #{backend => Backend})};
  364: required_module(Module, Opts) ->
  365:     {Module, mod_config(Module, Opts)}.
  366: 
  367: check_module_opt(Module, Key, Value) ->
  368:     case is_module_supported(Module) of
  369:         true ->
  370:             ?assertEqual(true, is_module_opt_reported(atom_to_binary(Module), Key, Value));
  371:         false ->
  372:             ct:log("Skipping unsupported module ~p", [Module])
  373:     end.
  374: 
  375: is_module_supported(Module) ->
  376:     is_host_type_static() orelse supports_dynamic_domains(Module).
  377: 
  378: is_host_type_static() ->
  379:     rpc(mim(), mongoose_domain_core, is_static, [host_type()]).
  380: 
  381: supports_dynamic_domains(Module) ->
  382:     rpc(mim(), gen_mod, does_module_support, [Module, dynamic_domains]).
  383: 
  384: all_event_have_the_same_client_id() ->
  385:     Tab = ets:tab2list(?ETS_TABLE),
  386:     UniqueSortedTab = lists:usort([Cid || #event{client_id = Cid} <- Tab]),
  387:     1 = length(UniqueSortedTab).
  388: 
  389: is_host_count_reported() ->
  390:     is_in_table(<<"host_count">>).
  391: 
  392: are_modules_reported() ->
  393:     is_in_table(<<"module">>).
  394: 
  395: is_in_table(EventName) ->
  396:     Tab = ets:tab2list(?ETS_TABLE),
  397:     lists:any(
  398:         fun(#event{name = Name, params = Params}) ->
  399:             verify_name(Name, EventName, Params)
  400:         end, Tab).
  401: 
  402: verify_name(<<"module_with_opt">>, <<"module">>, Params) ->
  403:     Module = maps:get(<<"module">>, Params),
  404:     Result = re:run(Module, "^mod_.*"),
  405:     case Result of
  406:         {match, _Captured} -> true;
  407:         nomatch -> false
  408:     end;
  409: verify_name(Name, Name, _) ->
  410:     true;
  411: verify_name(_, _, _) ->
  412:     false.
  413: 
  414: get_events_collection_size() ->
  415:     ets:info(?ETS_TABLE, size).
  416: 
  417: enable_system_metrics(Node) ->
  418:     enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100}).
  419: 
  420: enable_system_metrics_with_configurable_tracking_id(Node) ->
  421:     enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100,
  422:                                   tracking_id => ?TRACKING_ID_EXTRA}).
  423: 
  424: enable_system_metrics(Node, Opts) ->
  425:     UrlArgs = [google_analytics_url, ?SERVER_URL],
  426:     ok = mongoose_helper:successful_rpc(Node, mongoose_config, set_opt, UrlArgs),
  427:     start_system_metrics_service(Node, Opts).
  428: 
  429: start_system_metrics_service(Node, ExtraOpts) ->
  430:     Opts = config([services, service_mongoose_system_metrics], ExtraOpts),
  431:     dynamic_services:ensure_started(Node, service_mongoose_system_metrics, Opts).
  432: 
  433: disable_system_metrics(Node) ->
  434:     dynamic_services:ensure_stopped(Node, service_mongoose_system_metrics),
  435:     mongoose_helper:successful_rpc(Node, mongoose_config, unset_opt, [ google_analytics_url ]).
  436: 
  437: delete_prev_client_id(Node) ->
  438:     mongoose_helper:successful_rpc(Node, mnesia, delete_table, [service_mongoose_system_metrics]).
  439: 
  440: create_events_collection() ->
  441:     ets:new(?ETS_TABLE, [duplicate_bag, named_table, public]).
  442: 
  443: clear_events_collection() ->
  444:     ets:delete_all_objects(?ETS_TABLE).
  445: 
  446: system_metrics_service_is_enabled(Node) ->
  447:     Pid = distributed_helper:rpc(Node, erlang, whereis, [service_mongoose_system_metrics]),
  448:     erlang:is_pid(Pid).
  449: 
  450: system_metrics_service_is_disabled(Node) ->
  451:     not system_metrics_service_is_enabled(Node).
  452: 
  453: events_are_reported_to_primary_tracking_id() ->
  454:     events_are_reported_to_tracking_ids([primary_tracking_id()]).
  455: 
  456: events_are_reported_to_both_tracking_ids() ->
  457:     events_are_reported_to_tracking_ids([primary_tracking_id(), ?TRACKING_ID_EXTRA]).
  458: 
  459: primary_tracking_id() ->
  460:     case os:getenv("CI") of
  461:         "true" -> ?TRACKING_ID_CI;
  462:         _ -> ?TRACKING_ID
  463:     end.
  464: 
  465: events_are_reported_to_tracking_ids(ConfiguredTrackingIds) ->
  466:     Tab = ets:tab2list(?ETS_TABLE),
  467:     ActualTrackingIds = lists:usort([InstanceId || #event{instance_id = InstanceId} <- Tab]),
  468:     ExpectedTrackingIds = lists:sort([list_to_binary(Tid) || #{id := Tid} <- ConfiguredTrackingIds]),
  469:     ExpectedTrackingIds =:= ActualTrackingIds.
  470: 
  471: is_feature_reported(EventName, Key) ->
  472:     length(match_events(EventName, Key)) > 0.
  473: 
  474: is_feature_reported(EventName, Key, Value) ->
  475:     length(match_events(EventName, Key, Value)) > 0.
  476: 
  477: is_module_opt_reported(Module, Key, Value) ->
  478:     length(get_matched_events_for_module(Module, Key, Value)) > 0.
  479: 
  480: is_mongoose_version_reported() ->
  481:     is_feature_reported(<<"cluster">>, <<"version">>).
  482: 
  483: is_cluster_uptime_reported() ->
  484:     is_feature_reported(<<"cluster">>, <<"uptime">>).
  485: 
  486: are_xmpp_components_reported() ->
  487:     is_feature_reported(<<"cluster">>, <<"component">>).
  488: 
  489: is_config_type_reported() ->
  490:     IsToml = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"toml">>),
  491:     IsCfg = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"cfg">>),
  492:     IsToml orelse IsCfg.
  493: 
  494: is_api_reported() ->
  495:     is_in_table(<<"http_api">>).
  496: 
  497: are_transport_mechanisms_reported() ->
  498:     is_in_table(<<"transport_mechanism">>).
  499: 
  500: are_outgoing_pools_reported() ->
  501:     is_in_table(<<"outgoing_pool">>).
  502: 
  503: is_iq_count_reported() ->
  504:     is_feature_reported(<<"xmpp_stanza_count">>,
  505:                         <<"stanza_type">>,
  506:                         <<"xmppIqSent">>).
  507: 
  508: is_message_count_reported() ->
  509:     XmppMessageSent = is_feature_reported(<<"xmpp_stanza_count">>,
  510:                                           <<"stanza_type">>,
  511:                                           <<"xmppMessageSent">>),
  512:     XmppMessageReceived = is_feature_reported(<<"xmpp_stanza_count">>,
  513:                                                <<"stanza_type">>,
  514:                                                <<"xmppMessageReceived">>),
  515:     XmppMessageSent andalso XmppMessageReceived. 
  516: 
  517: assert_message_count_is_incremented(Sent, Received) ->
  518:     assert_increment(<<"xmppMessageSent">>, Sent),
  519:     assert_increment(<<"xmppMessageReceived">>, Received).
  520: 
  521: assert_increment(EventCategory, InitialValue) ->
  522:     Events = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory),
  523:     % expect exactly one event with an increment of 1
  524:     SeekedEvent = [Event || Event = #event{params = #{<<"total">> := Total, <<"increment">> := 1}}
  525:         <- Events, Total == InitialValue + 1],
  526:     ?assertMatch([_], SeekedEvent).
  527: 
  528: get_metric_value(EventCategory) ->
  529:     [#event{params = #{<<"total">> := Value}} | _] = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory),
  530:     Value.
  531: 
  532: more_than_one_component_is_reported() ->
  533:     Events = match_events(<<"cluster">>),
  534:     lists:any(fun(#event{params = Params}) ->
  535:                        maps:get(<<"component">>, Params) > 0
  536:               end, Events).
  537: 
  538: match_events(EventName) ->
  539:     ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}).
  540: 
  541: match_events(EventName, ParamKey) ->
  542:     Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}),
  543:     [Event || Event = #event{params = #{ParamKey := _}} <- Res].
  544: 
  545: match_events(EventName, ParamKey, ParamValue) ->
  546:     Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}),
  547:     [Event || Event = #event{params = #{ParamKey := Value}} <- Res, Value == ParamValue].
  548: 
  549: get_matched_events_for_module(ParamModule, Key, ParamValue) ->
  550:     Res = ets:match_object(?ETS_TABLE, #event{name = <<"module_with_opt">>, _ = '_'}),
  551:     [Event || Event = #event{params = #{<<"module">> := Module, Key := Value}} <- Res,
  552:          Value == ParamValue, Module == ParamModule].
  553: 
  554: %%--------------------------------------------------------------------
  555: %% Cowboy handlers
  556: %%--------------------------------------------------------------------
  557: handler_init(Req0) ->
  558:     {ok, Body, Req} = cowboy_req:read_body(Req0),
  559:     #{measurement_id := InstanceId, api_secret := AppSecret} = cowboy_req:match_qs([measurement_id, api_secret], Req0),
  560:     BodyMap = jiffy:decode(Body, [return_maps]),
  561:     EventTab = maps:get(<<"events">>, BodyMap), 
  562:     ClientID = maps:get(<<"client_id">>, BodyMap),
  563:     lists:map(
  564:         fun(Event) ->
  565:             EventRecord = #event{name = maps:get(<<"name">>, Event),
  566:                                  params = maps:get(<<"params">>, Event),
  567:                                  client_id = ClientID,
  568:                                  instance_id = InstanceId,
  569:                                  app_secret = AppSecret},
  570:             %% TODO there is a race condition when table is not available
  571:             ets:insert(?ETS_TABLE, EventRecord)
  572:         end, EventTab),
  573:     Req1 = cowboy_req:reply(200, #{}, <<>>, Req),
  574:     {ok, Req1, no_state}.