1: -module(graphql_metric_SUITE).
    2: 
    3: -include_lib("common_test/include/ct.hrl").
    4: -include_lib("eunit/include/eunit.hrl").
    5: -include_lib("exml/include/exml.hrl").
    6: 
    7: -compile([export_all, nowarn_export_all]).
    8: 
    9: -import(distributed_helper, [require_rpc_nodes/1, rpc/4]).
   10: -import(graphql_helper, [execute_auth/2, init_admin_handler/1]).
   11: 
   12: suite() ->
   13:     MIM2NodeName = maps:get(node, distributed_helper:mim2()),
   14:     %% Ensure nodes are connected
   15:     mongoose_helper:successful_rpc(net_kernel, connect_node, [MIM2NodeName]),
   16:     require_rpc_nodes([mim, mim2]) ++ escalus:suite().
   17: 
   18: all() ->
   19:      [{group, metrics}].
   20: 
   21: groups() ->
   22:      [{metrics, [], metrics_handler()}].
   23: 
   24: metrics_handler() ->
   25:     [get_all_metrics,
   26:      get_all_metrics_check_by_type,
   27:      get_by_name_global_erlang_metrics,
   28:      get_process_queue_length,
   29:      get_inet_stats,
   30:      get_vm_stats_memory,
   31:      get_metrics_as_dicts,
   32:      get_by_name_metrics_as_dicts,
   33:      get_metrics_as_dicts_with_key_one,
   34:      get_cluster_metrics,
   35:      get_by_name_cluster_metrics_as_dicts,
   36:      get_mim2_cluster_metrics].
   37: 
   38: init_per_suite(Config) ->
   39:     escalus:init_per_suite(init_admin_handler(Config)).
   40: 
   41: end_per_suite(Config) ->
   42:     escalus_fresh:clean(),
   43:     escalus:end_per_suite(Config).
   44: 
   45: init_per_testcase(CaseName, Config) ->
   46:      escalus:init_per_testcase(CaseName, Config).
   47: 
   48: end_per_testcase(CaseName, Config) ->
   49:      escalus:end_per_testcase(CaseName, Config).
   50: 
   51: get_all_metrics(Config) ->
   52:     %% Get all metrics
   53:     Result = execute_auth(#{query => get_all_metrics_call(),
   54:                             variables => #{}, operationName => <<"Q1">>}, Config),
   55:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
   56:     Map = maps:from_list([{Name, X} || X = #{<<"name">> := Name} <- ParsedResult]),
   57:     ReadsKey = [<<"global">>, <<"backends">>, <<"mod_roster">>, <<"read_roster_version">>],
   58:     Reads = maps:get(ReadsKey, Map),
   59:     %% Histogram integer keys have p prefix
   60:     check_histogram_p(Reads),
   61:     %% HistogramMetric type
   62:     #{<<"type">> := <<"histogram">>} = Reads.
   63: 
   64: get_all_metrics_check_by_type(Config) ->
   65:     %% Get all metrics
   66:     Result = execute_auth(#{query => get_all_metrics_call(),
   67:                             variables => #{}, operationName => <<"Q1">>}, Config),
   68:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
   69:     lists:foreach(fun check_metric_by_type/1, ParsedResult).
   70: 
   71: check_metric_by_type(#{<<"type">> := Type} = Map) ->
   72:     values_are_integers(Map, type_to_keys(Type)).
   73: 
   74: type_to_keys(<<"histogram">>) ->
   75:     [<<"n">>, <<"mean">>,  <<"min">>,  <<"max">>,  <<"median">>,
   76:      <<"p50">>, <<"p75">>, <<"p90">>, <<"p95">>,  <<"p99">>, <<"p999">>];
   77: type_to_keys(<<"counter">>) ->
   78:     [<<"value">>, <<"ms_since_reset">>];
   79: type_to_keys(<<"spiral">>) ->
   80:     [<<"one">>, <<"count">>];
   81: type_to_keys(<<"gauge">>) ->
   82:     [<<"value">>];
   83: type_to_keys(<<"merged_inet_stats">>) ->
   84:     [<<"connections">>, <<"recv_cnt">>, <<"recv_max">>, <<"recv_oct">>,
   85:      <<"send_cnt">>, <<"send_max">>, <<"send_oct">>, <<"send_pend">>];
   86: type_to_keys(<<"rdbms_stats">>) ->
   87:     [<<"workers">>, <<"recv_cnt">>, <<"recv_max">>, <<"recv_oct">>,
   88:      <<"send_cnt">>, <<"send_max">>, <<"send_oct">>, <<"send_pend">>];
   89: type_to_keys(<<"vm_stats_memory">>) ->
   90:     [<<"atom_used">>, <<"binary">>, <<"ets">>,
   91:      <<"processes_used">>, <<"system">>, <<"total">>];
   92: type_to_keys(<<"vm_system_info">>) ->
   93:     [<<"ets_limit">>, <<"port_count">>, <<"port_limit">>,
   94:      <<"process_count">>, <<"process_limit">>];
   95: type_to_keys(<<"probe_queues">>) ->
   96:     [<<"fsm">>, <<"regular">>, <<"total">>].
   97: 
   98: get_by_name_global_erlang_metrics(Config) ->
   99:     %% Filter by name works
  100:     Result = execute_auth(#{query => get_metrics_call_with_args(<<"(name: [\"global\", \"erlang\"])">>),
  101:                             variables => #{}, operationName => <<"Q1">>}, Config),
  102:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
  103:     Map = maps:from_list([{Name, X} || X = #{<<"name">> := Name} <- ParsedResult]),
  104:     Info = maps:get([<<"global">>, <<"erlang">>, <<"system_info">>], Map),
  105:     %% VMSystemInfoMetric type
  106:     #{<<"type">> := <<"vm_system_info">>} = Info,
  107:     check_metric_by_type(Info),
  108:     ReadsKey = [<<"global">>, <<"backends">>, <<"mod_roster">>, <<"read_roster_version">>],
  109:     %% Other metrics are filtered out
  110:     undef = maps:get(ReadsKey, Map, undef).
  111: 
  112: get_process_queue_length(Config) ->
  113:     Result = execute_auth(#{query => get_metrics_call_with_args(
  114:                                        <<"(name: [\"global\", \"processQueueLengths\"])">>),
  115:                             variables => #{}, operationName => <<"Q1">>}, Config),
  116:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
  117:     Map = maps:from_list([{Name, X} || X = #{<<"name">> := Name} <- ParsedResult]),
  118:     Lens = maps:get([<<"global">>, <<"processQueueLengths">>], Map),
  119:     %% ProbeQueuesMetric type
  120:     #{<<"type">> := <<"probe_queues">>} = Lens,
  121:     check_metric_by_type(Lens).
  122: 
  123: get_inet_stats(Config) ->
  124:     Result = execute_auth(#{query => get_metrics_call_with_args(
  125:                                        <<"(name: [\"global\", \"data\", \"dist\"])">>),
  126:                             variables => #{}, operationName => <<"Q1">>}, Config),
  127:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
  128:     Map = maps:from_list([{Name, X} || X = #{<<"name">> := Name} <- ParsedResult]),
  129:     Stats = maps:get([<<"global">>, <<"data">>, <<"dist">>], Map),
  130:     %% MergedInetStatsMetric type
  131:     #{<<"type">> := <<"merged_inet_stats">>} = Stats,
  132:     check_metric_by_type(Stats).
  133: 
  134: get_vm_stats_memory(Config) ->
  135:     Result = execute_auth(#{query => get_metrics_call_with_args(<<"(name: [\"global\"])">>),
  136:                             variables => #{}, operationName => <<"Q1">>}, Config),
  137:     ParsedResult = ok_result(<<"metric">>, <<"getMetrics">>, Result),
  138:     Map = maps:from_list([{Name, X} || X = #{<<"name">> := Name} <- ParsedResult]),
  139:     Mem = maps:get([<<"global">>, <<"erlang">>, <<"memory">>], Map),
  140:     %% VMStatsMemoryMetric type
  141:     #{<<"type">> := <<"vm_stats_memory">>} = Mem,
  142:     check_metric_by_type(Mem).
  143: 
  144: get_metrics_as_dicts(Config) ->
  145:     Result = execute_auth(#{query => get_all_metrics_as_dicts_call(), variables => #{},
  146:                             operationName => <<"Q1">>}, Config),
  147:     ParsedResult = ok_result(<<"metric">>, <<"getMetricsAsDicts">>, Result),
  148:     check_node_result_is_valid(ParsedResult, false).
  149: 
  150: get_by_name_metrics_as_dicts(Config) ->
  151:     Args = <<"(name: [\"_\", \"xmppStanzaSent\"])">>,
  152:     Result = execute_auth(#{query => get_by_args_metrics_as_dicts_call(Args),
  153:                             variables => #{}, operationName => <<"Q1">>}, Config),
  154:     ParsedResult = ok_result(<<"metric">>, <<"getMetricsAsDicts">>, Result),
  155:     [_|_] = ParsedResult,
  156:     %% Only xmppStanzaSent type
  157:     lists:foreach(fun(#{<<"dict">> := Dict, <<"name">> := [_, <<"xmppStanzaSent">>]}) ->
  158:                           check_spiral_dict(Dict)
  159:             end, ParsedResult).
  160: 
  161: get_metrics_as_dicts_with_key_one(Config) ->
  162:     Result = execute_auth(#{query => get_all_metrics_as_dicts_with_key_one_call(),
  163:                             variables => #{},
  164:                             operationName => <<"Q1">>}, Config),
  165:     ParsedResult = ok_result(<<"metric">>, <<"getMetricsAsDicts">>, Result),
  166:     Map = dict_objects_to_map(ParsedResult),
  167:     SentName = [metric_host_type(), <<"xmppStanzaSent">>],
  168:     [#{<<"key">> := <<"one">>, <<"value">> := One}] = maps:get(SentName, Map),
  169:     true = is_integer(One).
  170: 
  171: get_cluster_metrics(Config) ->
  172:     %% We will have at least these two nodes
  173:     Node1 = atom_to_binary(maps:get(node, distributed_helper:mim())),
  174:     Node2 = atom_to_binary(maps:get(node, distributed_helper:mim2())),
  175:     Result = execute_auth(#{query => get_all_cluster_metrics_as_dicts_call(),
  176:                             variables => #{},
  177:                             operationName => <<"Q1">>}, Config),
  178:     ParsedResult = ok_result(<<"metric">>, <<"getClusterMetricsAsDicts">>, Result),
  179:     #{Node1 := Res1, Node2 := Res2} = node_objects_to_map(ParsedResult),
  180:     check_node_result_is_valid(Res1, false),
  181:     check_node_result_is_valid(Res2, true).
  182: 
  183: get_by_name_cluster_metrics_as_dicts(Config) ->
  184:     Args = <<"(name: [\"_\", \"xmppStanzaSent\"])">>,
  185:     Result = execute_auth(#{query => get_by_args_cluster_metrics_as_dicts_call(Args),
  186:                             variables => #{}, operationName => <<"Q1">>}, Config),
  187:     NodeResult = ok_result(<<"metric">>, <<"getClusterMetricsAsDicts">>, Result),
  188:     Map = node_objects_to_map(NodeResult),
  189:     %% Contains data for at least two nodes
  190:     true = maps:size(Map) > 1,
  191:     %% Only xmppStanzaSent type
  192:     maps:map(fun(_Node, [_|_] = NodeRes) ->
  193:         lists:foreach(fun(#{<<"dict">> := Dict,
  194:                             <<"name">> := [_, <<"xmppStanzaSent">>]}) ->
  195:                               check_spiral_dict(Dict)
  196:                 end, NodeRes) end, Map).
  197: 
  198: get_mim2_cluster_metrics(Config) ->
  199:     Node = atom_to_binary(maps:get(node, distributed_helper:mim2())),
  200:     Result = execute_auth(#{query => get_node_cluster_metrics_as_dicts_call(Node),
  201:                             variables => #{},
  202:                             operationName => <<"Q1">>}, Config),
  203:     ParsedResult = ok_result(<<"metric">>, <<"getClusterMetricsAsDicts">>, Result),
  204:     [#{<<"node">> := Node, <<"result">> := ResList}] = ParsedResult,
  205:     check_node_result_is_valid(ResList, true).
  206: 
  207: check_node_result_is_valid(ResList, MetricsAreGlobal) ->
  208:     %% Check that result contains something
  209:     Map = dict_objects_to_map(ResList),
  210:     SentName = case MetricsAreGlobal of
  211:             true -> [<<"global">>, <<"xmppStanzaSent">>];
  212:             false -> [metric_host_type(), <<"xmppStanzaSent">>]
  213:         end,
  214:     check_spiral_dict(maps:get(SentName, Map)),
  215:     [#{<<"key">> := <<"value">>,<<"value">> := V}] =
  216:         maps:get([<<"global">>,<<"uniqueSessionCount">>], Map),
  217:     true = is_integer(V),
  218:     HistObjects = maps:get([<<"global">>, <<"data">>, <<"xmpp">>,
  219:                             <<"sent">>, <<"compressed_size">>], Map),
  220:     check_histogram(kv_objects_to_map(HistObjects)).
  221: 
  222: check_histogram(Map) ->
  223:     Keys = [<<"n">>, <<"mean">>,  <<"min">>,  <<"max">>,  <<"median">>,
  224:             <<"50">>, <<"75">>, <<"90">>, <<"95">>,  <<"99">>, <<"999">>],
  225:     values_are_integers(Map, Keys).
  226: 
  227: check_histogram_p(Map) ->
  228:     Keys = type_to_keys(<<"histogram">>),
  229:     values_are_integers(Map, Keys).
  230: 
  231: dict_objects_to_map(List) ->
  232:     KV = [{Name, Dict} || #{<<"name">> := Name, <<"dict">> := Dict} <- List],
  233:     maps:from_list(KV).
  234: 
  235: node_objects_to_map(List) ->
  236:     KV = [{Name, Value} || #{<<"node">> := Name, <<"result">> := Value} <- List],
  237:     maps:from_list(KV).
  238: 
  239: kv_objects_to_map(List) ->
  240:     KV = [{Key, Value} || #{<<"key">> := Key, <<"value">> := Value} <- List],
  241:     maps:from_list(KV).
  242: 
  243: get_all_metrics_call() ->
  244:     get_metrics_call_with_args(<<>>).
  245: 
  246: get_metrics_call_with_args(Args) ->
  247:     <<"query Q1
  248:            {metric
  249:                {getMetrics", Args/binary, " {
  250:                      ... on HistogramMetric
  251:                      { name type n mean min max median p50 p75 p90 p95 p99 p999 }
  252:                      ... on CounterMetric
  253:                      { name type value ms_since_reset }
  254:                      ... on SpiralMetric
  255:                      { name type one count }
  256:                      ... on GaugeMetric
  257:                      { name type value }
  258:                      ... on MergedInetStatsMetric
  259:                      { name type connections recv_cnt recv_max recv_oct
  260:                        send_cnt send_max send_oct send_pend }
  261:                      ... on RDBMSStatsMetric
  262:                      { name type workers recv_cnt recv_max recv_oct
  263:                        send_cnt send_max send_oct send_pend }
  264:                      ... on VMStatsMemoryMetric
  265:                      { name type total processes_used atom_used binary ets system }
  266:                      ... on VMSystemInfoMetric
  267:                      { name type port_count port_limit process_count process_limit ets_limit }
  268:                      ... on ProbeQueuesMetric
  269:                      { name type fsm regular total }
  270:                  }
  271:                }
  272:            }">>.
  273: 
  274: get_all_metrics_as_dicts_call() ->
  275:     get_by_args_metrics_as_dicts_call(<<>>).
  276: 
  277: get_by_args_metrics_as_dicts_call(Args) ->
  278:     <<"query Q1
  279:            {metric
  280:                {getMetricsAsDicts", Args/binary, " { name dict { key value }}}}">>.
  281: 
  282: get_all_metrics_as_dicts_with_key_one_call() ->
  283:     <<"query Q1
  284:            {metric
  285:                {getMetricsAsDicts(keys: [\"one\"]) { name dict { key value }}}}">>.
  286: 
  287: get_all_cluster_metrics_as_dicts_call() ->
  288:     get_by_args_cluster_metrics_as_dicts_call(<<>>).
  289: 
  290: get_by_args_cluster_metrics_as_dicts_call(Args) ->
  291:     <<"query Q1
  292:            {metric
  293:                {getClusterMetricsAsDicts", Args/binary,
  294:                " {node result { name dict { key value }}}}}">>.
  295: 
  296: get_node_cluster_metrics_as_dicts_call(NodeBin) ->
  297:     get_by_args_cluster_metrics_as_dicts_call(<<"(nodes: [\"", NodeBin/binary, "\"])">>).
  298: 
  299: %% Helpers
  300: ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) ->
  301:     maps:get(What2, maps:get(What1, Data)).
  302: 
  303: error_result(ErrorNumber, {{<<"200">>, <<"OK">>}, #{<<"errors">> := Errors}}) ->
  304:     lists:nth(ErrorNumber, Errors).
  305: 
  306: check_spiral_dict(Dict) ->
  307:     [#{<<"key">> := <<"count">>, <<"value">> := Count},
  308:      #{<<"key">> := <<"one">>, <<"value">> := One}] = Dict,
  309:     true = is_integer(Count),
  310:     true = is_integer(One).
  311: 
  312: values_are_integers(Map, Keys) ->
  313:     lists:foreach(fun(Key) -> true = is_integer(maps:get(Key, Map)) end, Keys).
  314: 
  315: metric_host_type() ->
  316:     binary:replace(domain_helper:host_type(), <<" ">>, <<"_">>, [global]).