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]).