|
| 1 | +%%============================================================================== |
| 2 | +%% Copyright 2016 Erlang Solutions Ltd. |
| 3 | +%% |
| 4 | +%% Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +%% you may not use this file except in compliance with the License. |
| 6 | +%% You may obtain a copy of the License at |
| 7 | +%% |
| 8 | +%% http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +%% |
| 10 | +%% Unless required by applicable law or agreed to in writing, software |
| 11 | +%% distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +%% See the License for the specific language governing permissions and |
| 14 | +%% limitations under the License. |
| 15 | +%% |
| 16 | +%% @doc Cowboy middleware updating metrics related to request handling |
| 17 | +%% |
| 18 | +%% This middleware needs to run after `mongoose_cowboy_metrics_mw_before' and `cowboy_router' |
| 19 | +%% middleware. However, it does not need to run after `cowboy_hander' middleware because metrics are |
| 20 | +%% updated in `onresponse' callback which is called whenever the request is sent. |
| 21 | +%% |
| 22 | +%% It is executed only if listener's env variable `record_metrics' is set to `true'. |
| 23 | +%% |
| 24 | +%% This middleware does not create any metrics, they need to be created earlier using |
| 25 | +%% `mongoose_metrics' module. Metric names used by the middleware are contructed as follows: |
| 26 | +%% - take some `Prefix', a list |
| 27 | +%% - take a request method, `Method', one of: |
| 28 | +%% - `<<"GET">>' |
| 29 | +%% - `<<"HEAD">>' |
| 30 | +%% - `<<"POST">>' |
| 31 | +%% - `<<"PUT">>' |
| 32 | +%% - `<<"DELETE">>' |
| 33 | +%% - `<<"OPTIONS">>' |
| 34 | +%% - `<<"PATCH">>' |
| 35 | +%% - take a request status class, `Class', and create a string like e.g. `<<"2XX">>' for success |
| 36 | +%% status codes |
| 37 | +%% - for each `Method' and `Class' define following metric names |
| 38 | +%% - `Prefix ++ [Method, request, count]' - updated by `1' whenever a request with method `Method' |
| 39 | +%% is about to be handled |
| 40 | +%% - `Prefix ++ [Method, response, Class, count]' - updated by `1' whenever a response of status |
| 41 | +%% class `Class' to a request with method `Method' is sent |
| 42 | +%% - `Prefix ++ [Method, response, Class, latency]' - updated by number of microseconds which |
| 43 | +%% passed since request timestamp was recorded by `mongoose_cowboy_metrics_mw_before' whenever |
| 44 | +%% a response of status class `Class' to a request with method `Method' is sent |
| 45 | +%% |
| 46 | +%% As you might have already guessed it makes sense to define `count' metrics as spirals, and |
| 47 | +%% `latency' metrics as histograms. The middleware will always try to update the metric regardless |
| 48 | +%% of whether it was created. Note that it's run after `cowboy_router' middleware, which means that |
| 49 | +%% error responses returned by the router (such as 404 for no matching handler) won't be recorded. |
| 50 | +%% |
| 51 | +%% And what about `Prefix'? By default prefix is the name of the handler handling the |
| 52 | +%% request wrapped in a list. However, you might provide `handler_to_metric_prefix' map as Cowboy |
| 53 | +%% listener environment value, where keys are handler names and values are corresponding prefixes. |
| 54 | +%% |
| 55 | +%% You can use functions from `mongoose_cowboy_metrics' module to generate names of metrics recorded |
| 56 | +%% by this module. |
| 57 | +%% |
| 58 | +%%============================================================================== |
| 59 | + |
| 60 | +-module(mongoose_cowboy_metrics_mw_after). |
| 61 | + |
| 62 | +-behaviour(cowboy_middleware). |
| 63 | + |
| 64 | +%% cowboy_middleware callbacks |
| 65 | +-export([execute/2]). |
| 66 | + |
| 67 | +%%------------------------------------------------------------------- |
| 68 | +%% cowboy_middleware callbacks |
| 69 | +%%------------------------------------------------------------------- |
| 70 | + |
| 71 | +execute(Req, Env) -> |
| 72 | + case proplists:get_value(record_metrics, Env, false) of |
| 73 | + true -> |
| 74 | + {req_timestamp, StartTs} = proplists:lookup(req_timestamp, Env), |
| 75 | + {handler, Handler} = proplists:lookup(handler, Env), |
| 76 | + Method = get_req_method(Req), |
| 77 | + HandlerToPrefixMappings = proplists:get_value(handler_to_metric_prefix, Env, #{}), |
| 78 | + Prefix = maps:get(Handler, HandlerToPrefixMappings, [Handler]), |
| 79 | + mongoose_metrics:update(global, mongoose_cowboy_metrics:request_count_metric(Prefix, Method), 1), |
| 80 | + OnResponse = on_response_fun(StartTs, Method, Prefix), |
| 81 | + {ok, cowboy_req:set([{onresponse, OnResponse}], Req), Env}; |
| 82 | + false -> |
| 83 | + {ok, Req, Env} |
| 84 | + end. |
| 85 | + |
| 86 | +%%------------------------------------------------------------------- |
| 87 | +%% Internals |
| 88 | +%%------------------------------------------------------------------- |
| 89 | + |
| 90 | +-spec on_response_fun(erlang:timestamp(), mongoose_cowboy_metrics:method(), |
| 91 | + mongoose_cowboy_metrics:prefix()) -> cowboy:onresponse_fun(). |
| 92 | +on_response_fun(StartTs, Method, Prefix) -> |
| 93 | + fun(Status, _Headers, _Body, RespReq) -> |
| 94 | + EndTs = erlang:timestamp(), |
| 95 | + Latency = calculate_latency(StartTs, EndTs), |
| 96 | + Class = calculate_status_class(Status), |
| 97 | + mongoose_metrics:update(global, mongoose_cowboy_metrics:response_count_metric(Prefix, Method, Class), 1), |
| 98 | + mongoose_metrics:update(global, mongoose_cowboy_metrics:response_latency_metric(Prefix, Method, Class), Latency), |
| 99 | + RespReq |
| 100 | + end. |
| 101 | + |
| 102 | +-spec calculate_latency(erlang:timestamp(), erlang:timestamp()) -> Microsecs :: non_neg_integer(). |
| 103 | +calculate_latency(StartTs, EndTs) -> |
| 104 | + timestamp_to_microsecs(EndTs) - timestamp_to_microsecs(StartTs). |
| 105 | + |
| 106 | +-spec timestamp_to_microsecs(erlang:timestamp()) -> Microsecs :: non_neg_integer(). |
| 107 | +timestamp_to_microsecs({MegaSecs, Secs, MicroSecs}) -> |
| 108 | + (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs. |
| 109 | + |
| 110 | +-spec get_req_method(cowboy_req:req()) -> mongoose_cowboy_metrics:method(). |
| 111 | +get_req_method(Req) -> |
| 112 | + {Method, _} = cowboy_req:method(Req), |
| 113 | + Method. |
| 114 | + |
| 115 | +-spec calculate_status_class(100..599) -> mongoose_cowboy_metrics:status_class(). |
| 116 | +calculate_status_class(S) when S >= 100, S < 200 -> <<"1XX">>; |
| 117 | +calculate_status_class(S) when S >= 200, S < 300 -> <<"2XX">>; |
| 118 | +calculate_status_class(S) when S >= 300, S < 400 -> <<"3XX">>; |
| 119 | +calculate_status_class(S) when S >= 400, S < 500 -> <<"4XX">>; |
| 120 | +calculate_status_class(S) when S >= 500, S < 600 -> <<"5XX">>. |
| 121 | + |
0 commit comments