mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00

This allows giving custom metadata to the metrics stream handler. This can be useful to for example provide the name of the module handling the request which is only known after routing. But any user data is allowed. When called multiple times the user data maps are merged.
331 lines
11 KiB
Erlang
331 lines
11 KiB
Erlang
%% 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).
|