|
|
- %% 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).
|