|
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
|
|
%%
|
|
%% Permission to use, copy, modify, and/or distribute this software for any
|
|
%% purpose with or without fee is hereby granted, provided that the above
|
|
%% copyright notice and this permission notice appear in all copies.
|
|
%%
|
|
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
-module(cowboy_metrics_h).
|
|
-behavior(cowboy_stream).
|
|
|
|
-export([init/3]).
|
|
-export([data/4]).
|
|
-export([info/3]).
|
|
-export([terminate/3]).
|
|
-export([early_error/5]).
|
|
|
|
-type proc_metrics() :: #{pid() => #{
|
|
%% Time at which the process spawned.
|
|
spawn := integer(),
|
|
|
|
%% Time at which the process exited.
|
|
exit => integer(),
|
|
|
|
%% Reason for the process exit.
|
|
reason => any()
|
|
}}.
|
|
|
|
-type informational_metrics() :: #{
|
|
%% Informational response status.
|
|
status := cowboy:http_status(),
|
|
|
|
%% Headers sent with the informational response.
|
|
headers := cowboy:http_headers(),
|
|
|
|
%% Time when the informational response was sent.
|
|
time := integer()
|
|
}.
|
|
|
|
-type metrics() :: #{
|
|
%% The identifier for this listener.
|
|
ref := ranch:ref(),
|
|
|
|
%% The pid for this connection.
|
|
pid := pid(),
|
|
|
|
%% The streamid also indicates the total number of requests on
|
|
%% this connection (StreamID div 2 + 1).
|
|
streamid := cowboy_stream:streamid(),
|
|
|
|
%% The terminate reason is always useful.
|
|
reason := cowboy_stream:reason(),
|
|
|
|
%% A filtered Req object or a partial Req object
|
|
%% depending on how far the request got to.
|
|
req => cowboy_req:req(),
|
|
partial_req => cowboy_stream:partial_req(),
|
|
|
|
%% Response status.
|
|
resp_status := cowboy:http_status(),
|
|
|
|
%% Filtered response headers.
|
|
resp_headers := cowboy:http_headers(),
|
|
|
|
%% Start/end of the processing of the request.
|
|
%%
|
|
%% This represents the time from this stream handler's init
|
|
%% to terminate.
|
|
req_start => integer(),
|
|
req_end => integer(),
|
|
|
|
%% Start/end of the receiving of the request body.
|
|
%% Begins when the first packet has been received.
|
|
req_body_start => integer(),
|
|
req_body_end => integer(),
|
|
|
|
%% Start/end of the sending of the response.
|
|
%% Begins when we send the headers and ends on the final
|
|
%% packet of the response body. If everything is sent at
|
|
%% once these values are identical.
|
|
resp_start => integer(),
|
|
resp_end => integer(),
|
|
|
|
%% For early errors all we get is the time we received it.
|
|
early_error_time => integer(),
|
|
|
|
%% Start/end of spawned processes. This is where most of
|
|
%% the user code lies, excluding stream handlers. On a
|
|
%% default Cowboy configuration there should be only one
|
|
%% process: the request process.
|
|
procs => proc_metrics(),
|
|
|
|
%% Informational responses sent before the final response.
|
|
informational => [informational_metrics()],
|
|
|
|
%% Length of the request and response bodies. This does
|
|
%% not include the framing.
|
|
req_body_length => non_neg_integer(),
|
|
resp_body_length => non_neg_integer(),
|
|
|
|
%% Additional metadata set by the user.
|
|
user_data => map()
|
|
}.
|
|
-export_type([metrics/0]).
|
|
|
|
-type metrics_callback() :: fun((metrics()) -> any()).
|
|
-export_type([metrics_callback/0]).
|
|
|
|
-record(state, {
|
|
next :: any(),
|
|
callback :: fun((metrics()) -> any()),
|
|
resp_headers_filter :: undefined | fun((cowboy:http_headers()) -> cowboy:http_headers()),
|
|
req :: map(),
|
|
resp_status :: undefined | cowboy:http_status(),
|
|
resp_headers :: undefined | cowboy:http_headers(),
|
|
ref :: ranch:ref(),
|
|
req_start :: integer(),
|
|
req_end :: undefined | integer(),
|
|
req_body_start :: undefined | integer(),
|
|
req_body_end :: undefined | integer(),
|
|
resp_start :: undefined | integer(),
|
|
resp_end :: undefined | integer(),
|
|
procs = #{} :: proc_metrics(),
|
|
informational = [] :: [informational_metrics()],
|
|
req_body_length = 0 :: non_neg_integer(),
|
|
resp_body_length = 0 :: non_neg_integer(),
|
|
user_data = #{} :: map()
|
|
}).
|
|
|
|
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
|
|
-> {[{spawn, pid(), timeout()}], #state{}}.
|
|
init(StreamID, Req = #{ref := Ref}, Opts = #{metrics_callback := Fun}) ->
|
|
ReqStart = erlang:monotonic_time(),
|
|
{Commands, Next} = cowboy_stream:init(StreamID, Req, Opts),
|
|
FilteredReq = case maps:get(metrics_req_filter, Opts, undefined) of
|
|
undefined -> Req;
|
|
ReqFilter -> ReqFilter(Req)
|
|
end,
|
|
RespHeadersFilter = maps:get(metrics_resp_headers_filter, Opts, undefined),
|
|
{Commands, fold(Commands, #state{
|
|
next = Next,
|
|
callback = Fun,
|
|
resp_headers_filter = RespHeadersFilter,
|
|
req = FilteredReq,
|
|
ref = Ref,
|
|
req_start = ReqStart
|
|
})}.
|
|
|
|
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
|
|
-> {cowboy_stream:commands(), State} when State :: #state{}.
|
|
data(StreamID, IsFin = fin, Data, State = #state{req_body_start = undefined}) ->
|
|
ReqBody = erlang:monotonic_time(),
|
|
do_data(StreamID, IsFin, Data, State#state{
|
|
req_body_start = ReqBody,
|
|
req_body_end = ReqBody,
|
|
req_body_length = byte_size(Data)
|
|
});
|
|
data(StreamID, IsFin = fin, Data, State = #state{req_body_length = ReqBodyLen}) ->
|
|
ReqBodyEnd = erlang:monotonic_time(),
|
|
do_data(StreamID, IsFin, Data, State#state{
|
|
req_body_end = ReqBodyEnd,
|
|
req_body_length = ReqBodyLen + byte_size(Data)
|
|
});
|
|
data(StreamID, IsFin, Data, State = #state{req_body_start = undefined}) ->
|
|
ReqBodyStart = erlang:monotonic_time(),
|
|
do_data(StreamID, IsFin, Data, State#state{
|
|
req_body_start = ReqBodyStart,
|
|
req_body_length = byte_size(Data)
|
|
});
|
|
data(StreamID, IsFin, Data, State = #state{req_body_length = ReqBodyLen}) ->
|
|
do_data(StreamID, IsFin, Data, State#state{
|
|
req_body_length = ReqBodyLen + byte_size(Data)
|
|
}).
|
|
|
|
do_data(StreamID, IsFin, Data, State0 = #state{next = Next0}) ->
|
|
{Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
|
|
{Commands, fold(Commands, State0#state{next = Next})}.
|
|
|
|
-spec info(cowboy_stream:streamid(), any(), State)
|
|
-> {cowboy_stream:commands(), State} when State :: #state{}.
|
|
info(StreamID, Info = {'EXIT', Pid, Reason}, State0 = #state{procs = Procs}) ->
|
|
ProcEnd = erlang:monotonic_time(),
|
|
P = maps:get(Pid, Procs),
|
|
State = State0#state{procs = Procs#{Pid => P#{
|
|
exit => ProcEnd,
|
|
reason => Reason
|
|
}}},
|
|
do_info(StreamID, Info, State);
|
|
info(StreamID, Info, State) ->
|
|
do_info(StreamID, Info, State).
|
|
|
|
do_info(StreamID, Info, State0 = #state{next = Next0}) ->
|
|
{Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
|
|
{Commands, fold(Commands, State0#state{next = Next})}.
|
|
|
|
fold([], State) ->
|
|
State;
|
|
fold([{spawn, Pid, _} | Tail], State0 = #state{procs = Procs}) ->
|
|
ProcStart = erlang:monotonic_time(),
|
|
State = State0#state{procs = Procs#{Pid => #{spawn => ProcStart}}},
|
|
fold(Tail, State);
|
|
fold([{inform, Status, Headers} | Tail],
|
|
State = #state{informational = Infos}) ->
|
|
Time = erlang:monotonic_time(),
|
|
fold(Tail, State#state{informational = [#{
|
|
status => Status,
|
|
headers => Headers,
|
|
time => Time
|
|
} | Infos]});
|
|
fold([{response, Status, Headers, Body} | Tail],
|
|
State = #state{resp_headers_filter = RespHeadersFilter}) ->
|
|
Resp = erlang:monotonic_time(),
|
|
fold(Tail, State#state{
|
|
resp_status = Status,
|
|
resp_headers = case RespHeadersFilter of
|
|
undefined -> Headers;
|
|
_ -> RespHeadersFilter(Headers)
|
|
end,
|
|
resp_start = Resp,
|
|
resp_end = Resp,
|
|
resp_body_length = resp_body_length(Body)
|
|
});
|
|
fold([{error_response, Status, Headers, Body} | Tail],
|
|
State = #state{resp_status = RespStatus}) ->
|
|
%% The error_response command only results in a response
|
|
%% if no response was sent before.
|
|
case RespStatus of
|
|
undefined ->
|
|
fold([{response, Status, Headers, Body} | Tail], State);
|
|
_ ->
|
|
fold(Tail, State)
|
|
end;
|
|
fold([{headers, Status, Headers} | Tail],
|
|
State = #state{resp_headers_filter = RespHeadersFilter}) ->
|
|
RespStart = erlang:monotonic_time(),
|
|
fold(Tail, State#state{
|
|
resp_status = Status,
|
|
resp_headers = case RespHeadersFilter of
|
|
undefined -> Headers;
|
|
_ -> RespHeadersFilter(Headers)
|
|
end,
|
|
resp_start = RespStart
|
|
});
|
|
%% @todo It might be worthwhile to keep the sendfile information around,
|
|
%% especially if these frames ultimately result in a sendfile syscall.
|
|
fold([{data, nofin, Data} | Tail], State = #state{resp_body_length = RespBodyLen}) ->
|
|
fold(Tail, State#state{
|
|
resp_body_length = RespBodyLen + resp_body_length(Data)
|
|
});
|
|
fold([{data, fin, Data} | Tail], State = #state{resp_body_length = RespBodyLen}) ->
|
|
RespEnd = erlang:monotonic_time(),
|
|
fold(Tail, State#state{
|
|
resp_end = RespEnd,
|
|
resp_body_length = RespBodyLen + resp_body_length(Data)
|
|
});
|
|
fold([{set_options, SetOpts} | Tail], State0 = #state{user_data = OldUserData}) ->
|
|
State = case SetOpts of
|
|
#{metrics_user_data := NewUserData} ->
|
|
State0#state{user_data = maps:merge(OldUserData, NewUserData)};
|
|
_ ->
|
|
State0
|
|
end,
|
|
fold(Tail, State);
|
|
fold([_ | Tail], State) ->
|
|
fold(Tail, State).
|
|
|
|
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
|
|
terminate(StreamID, Reason, #state{next = Next, callback = Fun,
|
|
req = Req, resp_status = RespStatus, resp_headers = RespHeaders, ref = Ref,
|
|
req_start = ReqStart, req_body_start = ReqBodyStart,
|
|
req_body_end = ReqBodyEnd, resp_start = RespStart, resp_end = RespEnd,
|
|
procs = Procs, informational = Infos, user_data = UserData,
|
|
req_body_length = ReqBodyLen, resp_body_length = RespBodyLen}) ->
|
|
Res = cowboy_stream:terminate(StreamID, Reason, Next),
|
|
ReqEnd = erlang:monotonic_time(),
|
|
Metrics = #{
|
|
ref => Ref,
|
|
pid => self(),
|
|
streamid => StreamID,
|
|
reason => Reason,
|
|
req => Req,
|
|
resp_status => RespStatus,
|
|
resp_headers => RespHeaders,
|
|
req_start => ReqStart,
|
|
req_end => ReqEnd,
|
|
req_body_start => ReqBodyStart,
|
|
req_body_end => ReqBodyEnd,
|
|
resp_start => RespStart,
|
|
resp_end => RespEnd,
|
|
procs => Procs,
|
|
informational => lists:reverse(Infos),
|
|
req_body_length => ReqBodyLen,
|
|
resp_body_length => RespBodyLen,
|
|
user_data => UserData
|
|
},
|
|
Fun(Metrics),
|
|
Res.
|
|
|
|
-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
|
|
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
|
|
when Resp :: cowboy_stream:resp_command().
|
|
early_error(StreamID, Reason, PartialReq = #{ref := Ref}, Resp0, Opts = #{metrics_callback := Fun}) ->
|
|
Time = erlang:monotonic_time(),
|
|
Resp = {response, RespStatus, RespHeaders, RespBody}
|
|
= cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp0, Opts),
|
|
%% As far as metrics go we are limited in what we can provide
|
|
%% in this case.
|
|
Metrics = #{
|
|
ref => Ref,
|
|
pid => self(),
|
|
streamid => StreamID,
|
|
reason => Reason,
|
|
partial_req => PartialReq,
|
|
resp_status => RespStatus,
|
|
resp_headers => RespHeaders,
|
|
early_error_time => Time,
|
|
resp_body_length => resp_body_length(RespBody)
|
|
},
|
|
Fun(Metrics),
|
|
Resp.
|
|
|
|
resp_body_length({sendfile, _, Len, _}) ->
|
|
Len;
|
|
resp_body_length(Data) ->
|
|
iolist_size(Data).
|