Skip to content

Commit bdbf593

Browse files
committed
Implement Cowboy middleware for calculating metrics
1 parent ad2c77e commit bdbf593

File tree

6 files changed

+430
-2
lines changed

6 files changed

+430
-2
lines changed

rebar.config

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
{observer_cli, "1.1.0"},
7373
{nkpacket, {git, "https://github.com/michalwski/nkpacket.git", {ref, "b6f8c73"}}},
7474
{nksip, {git, "https://github.com/NetComposer/nksip.git", {ref, "1a29ef3"}}},
75-
{eredis, {git, "https://github.com/wooga/eredis.git", {tag, "v1.0.8"}}}
75+
{eredis, {git, "https://github.com/wooga/eredis.git", {tag, "v1.0.8"}}},
76+
{gun, "1.0.0-pre.5"}
7677
]}.
7778

7879
{relx, [{release, { mongooseim, {cmd, "cat VERSION | tr -d '\r\n'"} },
@@ -126,7 +127,8 @@
126127
{fed1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/fed1.vars.config"]},
127128
{overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]},
128129
{reg1, [{relx, [ {overlay_vars, ["rel/vars.config", "rel/reg1.vars.config"]},
129-
{overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]}
130+
{overlay, [{template, "rel/files/ejabberd.cfg", "etc/ejabberd.cfg"}]} ]}]},
131+
{test, [{deps, [ {gun, "1.0.0-pre.5"}]}]}
130132
]}.
131133

132134
{plugins,

src/mongoose_cowboy_metrics.erl

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 Functions for generating names of metric's updated by `mongoose_cowboy_metrics_mw_after'
17+
%%
18+
%% To generate metric names use `request_count_metric/2', `response_latency_metric/2' and
19+
%% `response_count_metric/3'. See `mongoose_cowboy_metric_mw_after' module to check what values
20+
%% `Prefix', `Method' and `Class' may take.
21+
%%
22+
%%==============================================================================
23+
24+
-module(mongoose_cowboy_metrics).
25+
26+
%% API
27+
-export([request_count_metric/2]).
28+
-export([response_count_metric/3]).
29+
-export([response_latency_metric/3]).
30+
31+
-type prefix() :: list().
32+
-type method() :: binary(). %% <<"GET">>, <<"POST">>, etc.
33+
-type status_class() :: binary(). %% <<"2XX">>, <<"4XX">>, etc.
34+
-type metric_name() :: list().
35+
36+
-export_type([prefix/0]).
37+
-export_type([method/0]).
38+
-export_type([status_class/0]).
39+
-export_type([metric_name/0]).
40+
41+
%%-------------------------------------------------------------------
42+
%% API
43+
%%-------------------------------------------------------------------
44+
45+
-spec request_count_metric(prefix(), method()) -> metric_name().
46+
request_count_metric(Prefix, Method) ->
47+
Prefix ++ [Method, request, count].
48+
49+
-spec response_count_metric(prefix(), method(), status_class()) -> metric_name().
50+
response_count_metric(Prefix, Method, Class) ->
51+
Prefix ++ [Method, response, Class, count].
52+
53+
-spec response_latency_metric(prefix(), method(), status_class()) -> metric_name().
54+
response_latency_metric(Prefix, Method, Class) ->
55+
Prefix ++ [Method, response, Class, latency].
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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, one of the two responsible for recoding HTTP API metrics
17+
%%
18+
%% The job of this middleware is to record the timestamp of when the request comes in and to set.
19+
%%
20+
%% It's executed only if listener's env variable `record_metrics' is set to `true'.
21+
%%
22+
%% This middleware should be placed as soon as possible in the middleware chain, so that request
23+
%% timestamp of the request will be captured as quickly as possible.
24+
%%
25+
%%==============================================================================
26+
27+
-module(mongoose_cowboy_metrics_mw_before).
28+
29+
-behaviour(cowboy_middleware).
30+
31+
%% cowboy_middleware callbacks
32+
-export([execute/2]).
33+
34+
%%-------------------------------------------------------------------
35+
%% cowboy_middleware callbacks
36+
%%-------------------------------------------------------------------
37+
38+
execute(Req, Env) ->
39+
case proplists:get_value(record_metrics, Env, false) of
40+
true ->
41+
Ts = erlang:timestamp(),
42+
{ok, Req, [{req_timestamp, Ts} | Env]};
43+
false ->
44+
{ok, Req, Env}
45+
end.
46+

0 commit comments

Comments
 (0)