From 2ae5d2e8ed6022b13e6befa40e40daa7c9f57ee6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Gil Date: Fri, 6 Jul 2018 19:34:55 +0200 Subject: [PATCH 1/4] Implement Cowboy middleware for calculating metrics --- rebar.config | 3 +- src/mongoose_cowboy_metrics.erl | 55 +++++ src/mongoose_cowboy_metrics_mw_after.erl | 121 +++++++++++ src/mongoose_cowboy_metrics_mw_before.erl | 46 ++++ test/mongoose_cowboy_metrics_SUITE.erl | 201 ++++++++++++++++++ .../ejabberd.cfg | 3 + 6 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 src/mongoose_cowboy_metrics.erl create mode 100644 src/mongoose_cowboy_metrics_mw_after.erl create mode 100644 src/mongoose_cowboy_metrics_mw_before.erl create mode 100644 test/mongoose_cowboy_metrics_SUITE.erl create mode 100644 test/mongoose_cowboy_metrics_SUITE_data/ejabberd.cfg diff --git a/rebar.config b/rebar.config index b7abd63804d..d300e990679 100644 --- a/rebar.config +++ b/rebar.config @@ -126,7 +126,8 @@ {fed1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/fed1.vars.config"]}, {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]}, {reg1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/reg1.vars.config"]}, - {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]} + {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]}, + {test, [{deps, [ {gun, "1.0.0-pre.5"}]}]} ]}. {plugins, diff --git a/src/mongoose_cowboy_metrics.erl b/src/mongoose_cowboy_metrics.erl new file mode 100644 index 00000000000..cded894eb55 --- /dev/null +++ b/src/mongoose_cowboy_metrics.erl @@ -0,0 +1,55 @@ +%%============================================================================== +%% Copyright 2016 Erlang Solutions Ltd. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc Functions for generating names of metric's updated by `mongoose_cowboy_metrics_mw_after' +%% +%% To generate metric names use `request_count_metric/2', `response_latency_metric/2' and +%% `response_count_metric/3'. See `mongoose_cowboy_metric_mw_after' module to check what values +%% `Prefix', `Method' and `Class' may take. +%% +%%============================================================================== + +-module(mongoose_cowboy_metrics). + +%% API +-export([request_count_metric/2]). +-export([response_count_metric/3]). +-export([response_latency_metric/3]). + +-type prefix() :: list(). +-type method() :: binary(). %% <<"GET">>, <<"POST">>, etc. +-type status_class() :: binary(). %% <<"2XX">>, <<"4XX">>, etc. +-type metric_name() :: list(). + +-export_type([prefix/0]). +-export_type([method/0]). +-export_type([status_class/0]). +-export_type([metric_name/0]). + +%%------------------------------------------------------------------- +%% API +%%------------------------------------------------------------------- + +-spec request_count_metric(prefix(), method()) -> metric_name(). +request_count_metric(Prefix, Method) -> + Prefix ++ [Method, request, count]. + +-spec response_count_metric(prefix(), method(), status_class()) -> metric_name(). +response_count_metric(Prefix, Method, Class) -> + Prefix ++ [Method, response, Class, count]. + +-spec response_latency_metric(prefix(), method(), status_class()) -> metric_name(). +response_latency_metric(Prefix, Method, Class) -> + Prefix ++ [Method, response, Class, latency]. diff --git a/src/mongoose_cowboy_metrics_mw_after.erl b/src/mongoose_cowboy_metrics_mw_after.erl new file mode 100644 index 00000000000..81a4de2363f --- /dev/null +++ b/src/mongoose_cowboy_metrics_mw_after.erl @@ -0,0 +1,121 @@ +%%============================================================================== +%% Copyright 2016 Erlang Solutions Ltd. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc Cowboy middleware updating metrics related to request handling +%% +%% This middleware needs to run after `mongoose_cowboy_metrics_mw_before' and `cowboy_router' +%% middleware. However, it does not need to run after `cowboy_hander' middleware because metrics are +%% updated in `onresponse' callback which is called whenever the request is sent. +%% +%% It is executed only if listener's env variable `record_metrics' is set to `true'. +%% +%% This middleware does not create any metrics, they need to be created earlier using +%% `mongoose_metrics' module. Metric names used by the middleware are contructed as follows: +%% - take some `Prefix', a list +%% - take a request method, `Method', one of: +%% - `<<"GET">>' +%% - `<<"HEAD">>' +%% - `<<"POST">>' +%% - `<<"PUT">>' +%% - `<<"DELETE">>' +%% - `<<"OPTIONS">>' +%% - `<<"PATCH">>' +%% - take a request status class, `Class', and create a string like e.g. `<<"2XX">>' for success +%% status codes +%% - for each `Method' and `Class' define following metric names +%% - `Prefix ++ [Method, request, count]' - updated by `1' whenever a request with method `Method' +%% is about to be handled +%% - `Prefix ++ [Method, response, Class, count]' - updated by `1' whenever a response of status +%% class `Class' to a request with method `Method' is sent +%% - `Prefix ++ [Method, response, Class, latency]' - updated by number of microseconds which +%% passed since request timestamp was recorded by `mongoose_cowboy_metrics_mw_before' whenever +%% a response of status class `Class' to a request with method `Method' is sent +%% +%% As you might have already guessed it makes sense to define `count' metrics as spirals, and +%% `latency' metrics as histograms. The middleware will always try to update the metric regardless +%% of whether it was created. Note that it's run after `cowboy_router' middleware, which means that +%% error responses returned by the router (such as 404 for no matching handler) won't be recorded. +%% +%% And what about `Prefix'? By default prefix is the name of the handler handling the +%% request wrapped in a list. However, you might provide `handler_to_metric_prefix' map as Cowboy +%% listener environment value, where keys are handler names and values are corresponding prefixes. +%% +%% You can use functions from `mongoose_cowboy_metrics' module to generate names of metrics recorded +%% by this module. +%% +%%============================================================================== + +-module(mongoose_cowboy_metrics_mw_after). + +-behaviour(cowboy_middleware). + +%% cowboy_middleware callbacks +-export([execute/2]). + +%%------------------------------------------------------------------- +%% cowboy_middleware callbacks +%%------------------------------------------------------------------- + +execute(Req, Env) -> + case proplists:get_value(record_metrics, Env, false) of + true -> + {req_timestamp, StartTs} = proplists:lookup(req_timestamp, Env), + {handler, Handler} = proplists:lookup(handler, Env), + Method = get_req_method(Req), + HandlerToPrefixMappings = proplists:get_value(handler_to_metric_prefix, Env, #{}), + Prefix = maps:get(Handler, HandlerToPrefixMappings, [Handler]), + mongoose_metrics:update(global, mongoose_cowboy_metrics:request_count_metric(Prefix, Method), 1), + OnResponse = on_response_fun(StartTs, Method, Prefix), + {ok, cowboy_req:set([{onresponse, OnResponse}], Req), Env}; + false -> + {ok, Req, Env} + end. + +%%------------------------------------------------------------------- +%% Internals +%%------------------------------------------------------------------- + +-spec on_response_fun(erlang:timestamp(), mongoose_cowboy_metrics:method(), + mongoose_cowboy_metrics:prefix()) -> cowboy:onresponse_fun(). +on_response_fun(StartTs, Method, Prefix) -> + fun(Status, _Headers, _Body, RespReq) -> + EndTs = erlang:timestamp(), + Latency = calculate_latency(StartTs, EndTs), + Class = calculate_status_class(Status), + mongoose_metrics:update(global, mongoose_cowboy_metrics:response_count_metric(Prefix, Method, Class), 1), + mongoose_metrics:update(global, mongoose_cowboy_metrics:response_latency_metric(Prefix, Method, Class), Latency), + RespReq + end. + +-spec calculate_latency(erlang:timestamp(), erlang:timestamp()) -> Microsecs :: non_neg_integer(). +calculate_latency(StartTs, EndTs) -> + timestamp_to_microsecs(EndTs) - timestamp_to_microsecs(StartTs). + +-spec timestamp_to_microsecs(erlang:timestamp()) -> Microsecs :: non_neg_integer(). +timestamp_to_microsecs({MegaSecs, Secs, MicroSecs}) -> + (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs. + +-spec get_req_method(cowboy_req:req()) -> mongoose_cowboy_metrics:method(). +get_req_method(Req) -> + {Method, _} = cowboy_req:method(Req), + Method. + +-spec calculate_status_class(100..599) -> mongoose_cowboy_metrics:status_class(). +calculate_status_class(S) when S >= 100, S < 200 -> <<"1XX">>; +calculate_status_class(S) when S >= 200, S < 300 -> <<"2XX">>; +calculate_status_class(S) when S >= 300, S < 400 -> <<"3XX">>; +calculate_status_class(S) when S >= 400, S < 500 -> <<"4XX">>; +calculate_status_class(S) when S >= 500, S < 600 -> <<"5XX">>. + diff --git a/src/mongoose_cowboy_metrics_mw_before.erl b/src/mongoose_cowboy_metrics_mw_before.erl new file mode 100644 index 00000000000..09fe0a1ba7f --- /dev/null +++ b/src/mongoose_cowboy_metrics_mw_before.erl @@ -0,0 +1,46 @@ +%%============================================================================== +%% Copyright 2016 Erlang Solutions Ltd. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc Cowboy middleware, one of the two responsible for recoding HTTP API metrics +%% +%% The job of this middleware is to record the timestamp of when the request comes in and to set. +%% +%% It's executed only if listener's env variable `record_metrics' is set to `true'. +%% +%% This middleware should be placed as soon as possible in the middleware chain, so that request +%% timestamp of the request will be captured as quickly as possible. +%% +%%============================================================================== + +-module(mongoose_cowboy_metrics_mw_before). + +-behaviour(cowboy_middleware). + +%% cowboy_middleware callbacks +-export([execute/2]). + +%%------------------------------------------------------------------- +%% cowboy_middleware callbacks +%%------------------------------------------------------------------- + +execute(Req, Env) -> + case proplists:get_value(record_metrics, Env, false) of + true -> + Ts = erlang:timestamp(), + {ok, Req, [{req_timestamp, Ts} | Env]}; + false -> + {ok, Req, Env} + end. + diff --git a/test/mongoose_cowboy_metrics_SUITE.erl b/test/mongoose_cowboy_metrics_SUITE.erl new file mode 100644 index 00000000000..212cb6dca27 --- /dev/null +++ b/test/mongoose_cowboy_metrics_SUITE.erl @@ -0,0 +1,201 @@ +-module(mongoose_cowboy_metrics_SUITE). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-import(mongoose_cowboy_metrics, [request_count_metric/2, + response_count_metric/2, + response_count_metric/3, + response_latency_metric/3]). + +-define(LISTENER, mongoose_cowboy_metrics_SUITE_listener). +-define(HANDLER, ?MODULE). +-define(METHODS, [<<"GET">>, + <<"HEAD">>, + <<"POST">>, + <<"PUT">>, + <<"DELETE">>, + <<"OPTIONS">>, + <<"PATCH">>]). +-define(DEFAULT_PREFIX, [?HANDLER]). +-define(CLASSES, [<<"1XX">>, <<"2XX">>, <<"3XX">>, <<"4XX">>, <<"5XX">>]). + + + +%%------------------------------------------------------------------- +%% Suite definition +%%------------------------------------------------------------------- + +all() -> + [metrics_are_not_recorded_by_default, + metrics_are_not_recorded_when_record_metrics_is_false, + metrics_are_recorded, + metrics_are_recorded_with_configured_prefix, + unsent_responses_generate_metrics + ]. + +%%------------------------------------------------------------------- +%% Init & teardown +%%------------------------------------------------------------------- + +init_per_suite(Config) -> + application:ensure_all_started(gun), + ejabberd_helper:start_ejabberd_with_config(Config, "ejabberd.cfg"), + Config. + +end_per_suite(_Config) -> + application:stop(gun), + ejabberd_helper:stop_ejabberd(), + ok. + +%%------------------------------------------------------------------- +%% Test cases +%%------------------------------------------------------------------- + +metrics_are_not_recorded_by_default(_Config) -> + create_metrics(), + start_listener([{'_', ?HANDLER, []}], []), + + metrics_are_not_recorded(), + + stop_listener(), + destroy_metrics(). + +metrics_are_not_recorded_when_record_metrics_is_false(_Config) -> + create_metrics(), + start_listener([{'_', ?HANDLER, []}], [{record_metrics, false}]), + + metrics_are_not_recorded(), + + stop_listener(), + destroy_metrics(). + +metrics_are_not_recorded() -> + run_requests([get, head, post, put, delete, options, patch], + [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + [ensure_metric_value(M, 0) + || M <- count_metrics(?DEFAULT_PREFIX) ++ latency_metrics(?DEFAULT_PREFIX)]. + +metrics_are_recorded(_Config) -> + create_metrics(), + start_listener([{'_', ?HANDLER, []}], [{record_metrics, true}]), + + timer:sleep(1000), + run_requests([get, head, post, put, delete, options, patch], + [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + + [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), 5) || M <- ?METHODS], + [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, C), 1) || M <- ?METHODS, C <- ?CLASSES], + [ensure_metric_bumped(response_latency_metric(?DEFAULT_PREFIX, M, C)) || M <- ?METHODS, C <- ?CLASSES], + + stop_listener(), + destroy_metrics(). + + +metrics_are_recorded_with_configured_prefix(_Config) -> + Prefix = [http, api, my_endpoint], + create_metrics(Prefix), + start_listener([{'_', ?HANDLER, []}], [{record_metrics, true}, + {handler_to_metric_prefix, #{?HANDLER => Prefix}}]), + + run_requests([get, head, post, put, delete, options, patch], + [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + + [ensure_metric_value(request_count_metric(Prefix, M), 5) || M <- ?METHODS], + [ensure_metric_value(response_count_metric(Prefix, M, C), 1) || M <- ?METHODS, C <- ?CLASSES], + [ensure_metric_bumped(response_latency_metric(Prefix, M, C)) || M <- ?METHODS, C <- ?CLASSES], + + stop_listener(), + destroy_metrics(Prefix). + +unsent_responses_generate_metrics(_Config) -> + create_metrics(), + start_listener([{'_', ?HANDLER, [{action, none}]}], [{record_metrics, true}]), + + run_requests([get, head, post, put, delete, options, patch], + [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + + %% when response is not sent Cowboy returns 204 + [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), 5) || M <- ?METHODS], + [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, <<"2XX">>), 5) || M <- ?METHODS], + [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, C), 0) || M <- ?METHODS, C <- ?CLASSES -- [<<"2XX">>]], + [ensure_metric_bumped(response_latency_metric(?DEFAULT_PREFIX, M, <<"2XX">>)) || M <- ?METHODS], + [ensure_metric_value(response_latency_metric(?DEFAULT_PREFIX, M, C), 0) || M <- ?METHODS, C <- ?CLASSES -- [<<"2XX">>]], + + stop_listener(), + destroy_metrics(). + +%%------------------------------------------------------------------- +%% cowboy handler callbacks +%%------------------------------------------------------------------- + +init(_Type, Req, Opts) -> + {ok, Req, proplists:get_value(action, Opts, echo_status)}. + +handle(Req, echo_status = State) -> + {BinStatus, Req1} = cowboy_req:qs_val(<<"status">>, Req), + Status = binary_to_integer(BinStatus), + {ok, RespReq} = cowboy_req:reply(Status, [], <<>>, Req1), + {ok, RespReq, State}; +handle(Req, none = State) -> + {ok, Req, State}. + +terminate(_Reason, _Req, _State) -> + ok. + +%%------------------------------------------------------------------- +%% Helpers +%%------------------------------------------------------------------- + +start_listener(Paths, ExtraEnv) -> + Dispatch = cowboy_router:compile([{'_', Paths}]), + Middleware = [mongoose_cowboy_metrics_mw_before, + cowboy_router, + mongoose_cowboy_metrics_mw_after, + cowboy_handler], + {ok, _} = cowboy:start_http(?LISTENER, 1, [{port, 0}], + [{env, [{dispatch, Dispatch} | ExtraEnv]}, + {middlewares, Middleware}]). + +stop_listener() -> + ok = cowboy:stop_listener(?LISTENER). + +create_metrics() -> + create_metrics(?DEFAULT_PREFIX). + +create_metrics(Prefix) -> + %% in case of request count we're interested in the exact values - let's use counter + [ok = mongoose_metrics:ensure_metric(global, M, counter) || M <- count_metrics(Prefix)], + %% in case of latencies it doesn't make sense to check if values are correct - we + %% only need to know if the value has been reported, so gauge is enough + [ok = mongoose_metrics:ensure_metric(global, M, gauge) || M <- latency_metrics(Prefix)]. + +destroy_metrics() -> + destroy_metrics(?DEFAULT_PREFIX). + +destroy_metrics(Prefix) -> + [exometer:delete([global | M]) || M <- count_metrics(Prefix) ++ latency_metrics(Prefix)]. + +count_metrics(Prefix) -> + [request_count_metric(Prefix, M) || M <- ?METHODS] ++ + [response_count_metric(Prefix, M, C) || M <- ?METHODS, C <- ?CLASSES]. + +latency_metrics(Prefix) -> + [response_latency_metric(Prefix, M, C) || M <- ?METHODS, C <- ?CLASSES]. + +ensure_metric_value(Metric, Value) -> + {ok, DataPoints} = mongoose_metrics:get_metric_value([global | Metric]), + ?assertEqual(Value, proplists:get_value(value, DataPoints)). + +ensure_metric_bumped(Metric) -> + {ok, DataPoints} = mongoose_metrics:get_metric_value([global | Metric]), + ?assertNotEqual(0, proplists:get_value(value, DataPoints)). + +run_requests(Methods, Statuses) -> + {ok, Conn} = gun:open("localhost", ranch:get_port(?LISTENER)), + {ok, _} = gun:await_up(Conn), + [begin + StreamRef = gun:M(Conn, <<"/?status=", S/binary>>, []), + {response, fin, _, _} = gun:await(Conn, StreamRef) + end || M <- Methods, S <- Statuses]. + diff --git a/test/mongoose_cowboy_metrics_SUITE_data/ejabberd.cfg b/test/mongoose_cowboy_metrics_SUITE_data/ejabberd.cfg new file mode 100644 index 00000000000..7dbdf739510 --- /dev/null +++ b/test/mongoose_cowboy_metrics_SUITE_data/ejabberd.cfg @@ -0,0 +1,3 @@ +{hosts, ["localhost"]}. + +{sm_backend, {mnesia, []}}. From 3b141a1f50add4abdf2a5a60e324ae091d21ffe8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Gil Date: Mon, 9 Jul 2018 10:09:49 +0200 Subject: [PATCH 2/4] Define HTTP metrics when starting ejabberd_cowboy --- src/ejabberd_cowboy.erl | 78 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/src/ejabberd_cowboy.erl b/src/ejabberd_cowboy.erl index 47b801c939f..b5afbeac02a 100644 --- a/src/ejabberd_cowboy.erl +++ b/src/ejabberd_cowboy.erl @@ -130,8 +130,9 @@ do_start_cowboy(Ref, Opts) -> TransportOpts = gen_mod:get_opt(transport_options, Opts, []), Modules = gen_mod:get_opt(modules, Opts), Dispatch = cowboy_router:compile(get_routes(Modules)), - ProtocolOpts = [{env, [{dispatch, Dispatch}]} | - gen_mod:get_opt(protocol_options, Opts, [])], + {MetricsEnv, MetricsProtoOpts} = maybe_init_metrics(Opts), + ProtocolOpts = [{env, [{dispatch, Dispatch} | MetricsEnv]} | + gen_mod:get_opt(protocol_options, Opts, [])] ++ MetricsProtoOpts, case catch start_http_or_https(SSLOpts, Ref, NumAcceptors, TransportOpts, ProtocolOpts) of {error, {{shutdown, {failed_to_start_child, ranch_acceptors_sup, @@ -180,7 +181,9 @@ get_routes([], Routes) -> Routes; get_routes([{Host, BasePath, Module} | Tail], Routes) -> get_routes([{Host, BasePath, Module, []} | Tail], Routes); -get_routes([{Host, BasePath, Module, Opts} | Tail], Routes) -> +get_routes([{Host, BasePath, Module, HandlerOpts} | Tail], Routes) -> + get_routes([{Host, BasePath, Module, HandlerOpts, []} | Tail], Routes); +get_routes([{Host, BasePath, Module, HandlerOpts, _Opts} | Tail], Routes) -> %% ejabberd_config tries to expand the atom '_' as a Macro, which fails. %% To work around that, use "_" instead and translate it to '_' here. CowboyHost = case Host of @@ -190,8 +193,8 @@ get_routes([{Host, BasePath, Module, Opts} | Tail], Routes) -> {module, Module} = code:ensure_loaded(Module), Paths = proplists:get_value(CowboyHost, Routes, []) ++ case erlang:function_exported(Module, cowboy_router_paths, 2) of - true -> Module:cowboy_router_paths(BasePath, Opts); - _ -> [{BasePath, Module, Opts}] + true -> Module:cowboy_router_paths(BasePath, HandlerOpts); + _ -> [{BasePath, Module, HandlerOpts}] end, get_routes(Tail, lists:keystore(CowboyHost, 1, Routes, {CowboyHost, Paths})). @@ -224,3 +227,68 @@ maybe_insert_max_connections(TransportOpts, Opts) -> NewTuple = {Key, Value}, lists:keystore(Key, 1, TransportOpts, NewTuple) end. + +-spec measured_methods() -> [mongoose_cowboy_metrics:method()]. +measured_methods() -> + [<<"GET">>, + <<"HEAD">>, + <<"POST">>, + <<"PUT">>, + <<"DELETE">>, + <<"OPTIONS">>, + <<"PATCH">>]. + +-spec measured_classes() -> [mongoose_cowboy_metrics:status_class()]. +measured_classes() -> + [<<"1XX">>, <<"2XX">>, <<"3XX">>, <<"4XX">>, <<"5XX">>]. + +base_metrics_prefix() -> + [http]. + +middlewares_with_metrics() -> + [mongoose_cowboy_metrics_mw_before, + cowboy_router, + mongoose_cowboy_metrics_mw_after, + cowboy_handler]. + +-spec maybe_init_metrics(list()) -> {MetricsEnv :: list(), MetricsProtocolOpts :: list()}. +maybe_init_metrics(Opts) -> + case proplists:get_value(metrics, Opts, false) of + true -> + BasePrefix = base_metrics_prefix(), + HandlerToPrefixMappings = build_metric_prefixes( + BasePrefix, proplists:get_value(modules, Opts, []), #{}), + [create_metrics(Prefix) || Prefix <- maps:values(HandlerToPrefixMappings)], + {[{record_metrics, true}, {handler_to_metric_prefix, HandlerToPrefixMappings}], + [{middlewares, middlewares_with_metrics()}]}; + false -> + {[], []} + end. + +-spec build_metric_prefixes(BasePrefix :: list(), Modules :: [tuple()], Acc) -> Acc + when Acc :: #{module() => mongoose_cowboy_metrics:prefix()}. +build_metric_prefixes(_BasePrefix, [], Acc) -> + Acc; +build_metric_prefixes(BasePrefix, [{_Host, _Path, Handler, _HandlerOpts, Opts} | Tail], Acc) -> + case proplists:get_value(metrics, Opts, []) of + MetricsOpts when is_list(MetricsOpts) -> + HandlerPrefix = proplists:get_value(prefix, MetricsOpts, Handler), + Prefix = BasePrefix ++ lists:flatten([HandlerPrefix]), + build_metric_prefixes(BasePrefix, Tail, maps:put(Handler, Prefix, Acc)); + false -> + build_metric_prefixes(BasePrefix, Tail, Acc) + end; +build_metric_prefixes(BasePrefix, [{Host, Path, Handler, HandlerOpts} | Tail], Acc) -> + build_metric_prefixes(BasePrefix, [{Host, Path, Handler, HandlerOpts, []} | Tail], Acc); +build_metric_prefixes(BasePrefix, [{Host, Path, Handler} | Tail], Acc) -> + build_metric_prefixes(BasePrefix, [{Host, Path, Handler, [], []} | Tail], Acc). + +-spec create_metrics(mongoose_cowboy_metrics:prefix()) -> any(). +create_metrics(Prefix) -> + CountMetrics = [mongoose_cowboy_metrics:request_count_metric(Prefix, M) || M <- measured_methods()] ++ + [mongoose_cowboy_metrics:response_count_metric(Prefix, M, C) + || M <- measured_methods(), C <- measured_classes()], + LatencyMetrics = [mongoose_cowboy_metrics:response_latency_metric(Prefix, M, C) + || M <- measured_methods(), C <- measured_classes()], + [mongoose_metrics:ensure_metric(global, M, spiral) || M <- CountMetrics], + [mongoose_metrics:ensure_metric(global, M, histogram) || M <- LatencyMetrics]. From 3bddde0b98c4cd53c228b67e20bb1621222a1cee Mon Sep 17 00:00:00 2001 From: Arkadiusz Gil Date: Mon, 9 Jul 2018 13:43:11 +0200 Subject: [PATCH 3/4] Init metric subscriptions after listeners are started This way reporters will be automatically subscribed to all HTTP metrics. --- src/ejabberd_app.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index ed87e88e703..f21214d2978 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -72,8 +72,8 @@ start(normal, _Args) -> %%ejabberd_debug:fprof_start(), start_services(), start_modules(), - mongoose_metrics:init(), ejabberd_listener:start_listeners(), + mongoose_metrics:init(), ejabberd_admin:start(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?MONGOOSE_VERSION, node()]), Sup; From e18e89c9f61e6b4ef7631db8cd927098643c9784 Mon Sep 17 00:00:00 2001 From: kanes115 Date: Tue, 17 Jul 2018 13:36:32 +0200 Subject: [PATCH 4/4] Replace gun with fusco --- rebar.config | 3 +- test/mongoose_cowboy_metrics_SUITE.erl | 52 +++++++++++++++----------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/rebar.config b/rebar.config index d300e990679..b7abd63804d 100644 --- a/rebar.config +++ b/rebar.config @@ -126,8 +126,7 @@ {fed1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/fed1.vars.config"]}, {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]}, {reg1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/reg1.vars.config"]}, - {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]}, - {test, [{deps, [ {gun, "1.0.0-pre.5"}]}]} + {overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]} ]}. {plugins, diff --git a/test/mongoose_cowboy_metrics_SUITE.erl b/test/mongoose_cowboy_metrics_SUITE.erl index 212cb6dca27..f741e44bf4f 100644 --- a/test/mongoose_cowboy_metrics_SUITE.erl +++ b/test/mongoose_cowboy_metrics_SUITE.erl @@ -18,7 +18,7 @@ <<"OPTIONS">>, <<"PATCH">>]). -define(DEFAULT_PREFIX, [?HANDLER]). --define(CLASSES, [<<"1XX">>, <<"2XX">>, <<"3XX">>, <<"4XX">>, <<"5XX">>]). +-define(CLASSES, [<<"2XX">>, <<"3XX">>, <<"4XX">>, <<"5XX">>]). @@ -39,12 +39,10 @@ all() -> %%------------------------------------------------------------------- init_per_suite(Config) -> - application:ensure_all_started(gun), ejabberd_helper:start_ejabberd_with_config(Config, "ejabberd.cfg"), Config. end_per_suite(_Config) -> - application:stop(gun), ejabberd_helper:stop_ejabberd(), ok. @@ -71,8 +69,8 @@ metrics_are_not_recorded_when_record_metrics_is_false(_Config) -> destroy_metrics(). metrics_are_not_recorded() -> - run_requests([get, head, post, put, delete, options, patch], - [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + run_requests([get, head, post, put, delete, options, patch], + [<<"200">>, <<"300">>, <<"400">>, <<"500">>]), [ensure_metric_value(M, 0) || M <- count_metrics(?DEFAULT_PREFIX) ++ latency_metrics(?DEFAULT_PREFIX)]. @@ -81,10 +79,11 @@ metrics_are_recorded(_Config) -> start_listener([{'_', ?HANDLER, []}], [{record_metrics, true}]), timer:sleep(1000), + Statuses = [<<"200">>, <<"300">>, <<"400">>, <<"500">>], run_requests([get, head, post, put, delete, options, patch], - [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + Statuses), - [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), 5) || M <- ?METHODS], + [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), length(Statuses)) || M <- ?METHODS], [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, C), 1) || M <- ?METHODS, C <- ?CLASSES], [ensure_metric_bumped(response_latency_metric(?DEFAULT_PREFIX, M, C)) || M <- ?METHODS, C <- ?CLASSES], @@ -98,10 +97,11 @@ metrics_are_recorded_with_configured_prefix(_Config) -> start_listener([{'_', ?HANDLER, []}], [{record_metrics, true}, {handler_to_metric_prefix, #{?HANDLER => Prefix}}]), + Statuses = [<<"200">>, <<"300">>, <<"400">>, <<"500">>], run_requests([get, head, post, put, delete, options, patch], - [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + Statuses), - [ensure_metric_value(request_count_metric(Prefix, M), 5) || M <- ?METHODS], + [ensure_metric_value(request_count_metric(Prefix, M), length(Statuses)) || M <- ?METHODS], [ensure_metric_value(response_count_metric(Prefix, M, C), 1) || M <- ?METHODS, C <- ?CLASSES], [ensure_metric_bumped(response_latency_metric(Prefix, M, C)) || M <- ?METHODS, C <- ?CLASSES], @@ -112,12 +112,13 @@ unsent_responses_generate_metrics(_Config) -> create_metrics(), start_listener([{'_', ?HANDLER, [{action, none}]}], [{record_metrics, true}]), + Statuses = [<<"200">>, <<"300">>, <<"400">>, <<"500">>], run_requests([get, head, post, put, delete, options, patch], - [<<"100">>, <<"200">>, <<"300">>, <<"400">>, <<"500">>]), + Statuses), %% when response is not sent Cowboy returns 204 - [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), 5) || M <- ?METHODS], - [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, <<"2XX">>), 5) || M <- ?METHODS], + [ensure_metric_value(request_count_metric(?DEFAULT_PREFIX, M), length(Statuses)) || M <- ?METHODS], + [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, <<"2XX">>), length(Statuses)) || M <- ?METHODS], [ensure_metric_value(response_count_metric(?DEFAULT_PREFIX, M, C), 0) || M <- ?METHODS, C <- ?CLASSES -- [<<"2XX">>]], [ensure_metric_bumped(response_latency_metric(?DEFAULT_PREFIX, M, <<"2XX">>)) || M <- ?METHODS], [ensure_metric_value(response_latency_metric(?DEFAULT_PREFIX, M, C), 0) || M <- ?METHODS, C <- ?CLASSES -- [<<"2XX">>]], @@ -135,12 +136,12 @@ init(_Type, Req, Opts) -> handle(Req, echo_status = State) -> {BinStatus, Req1} = cowboy_req:qs_val(<<"status">>, Req), Status = binary_to_integer(BinStatus), - {ok, RespReq} = cowboy_req:reply(Status, [], <<>>, Req1), + {ok, RespReq} = cowboy_req:reply(Status, Req1), {ok, RespReq, State}; handle(Req, none = State) -> {ok, Req, State}. -terminate(_Reason, _Req, _State) -> +terminate(Reason, _Req, _State) -> ok. %%------------------------------------------------------------------- @@ -192,10 +193,19 @@ ensure_metric_bumped(Metric) -> ?assertNotEqual(0, proplists:get_value(value, DataPoints)). run_requests(Methods, Statuses) -> - {ok, Conn} = gun:open("localhost", ranch:get_port(?LISTENER)), - {ok, _} = gun:await_up(Conn), - [begin - StreamRef = gun:M(Conn, <<"/?status=", S/binary>>, []), - {response, fin, _, _} = gun:await(Conn, StreamRef) - end || M <- Methods, S <- Statuses]. - + Port = ranch:get_port(?LISTENER), + {ok, Conn} = fusco:start_link({"localhost", Port, false}, []), + Res = [{ok, _Result} = fusco:request(Conn, <<"/?status=", S/binary>>, method_to_upper_bin(M), [], [], 5000) + || M <- Methods, S <- Statuses], + fusco:disconnect(Conn), + Res. + +method_to_upper_bin(Method) when is_binary(Method) -> + Method; +method_to_upper_bin(Method) when is_atom(Method) -> + MethodBin = atom_to_binary(Method, utf8), + list_to_binary( + string:to_upper( + binary_to_list(MethodBin) + ) + ).