cowboy_metrics_h.erl 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. %% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(cowboy_metrics_h).
  15. -behavior(cowboy_stream).
  16. -export([init/3]).
  17. -export([data/4]).
  18. -export([info/3]).
  19. -export([terminate/3]).
  20. -export([early_error/5]).
  21. -type proc_metrics() :: #{pid() => #{
  22. %% Time at which the process spawned.
  23. spawn := integer(),
  24. %% Time at which the process exited.
  25. exit => integer(),
  26. %% Reason for the process exit.
  27. reason => any()
  28. }}.
  29. -type informational_metrics() :: #{
  30. %% Informational response status.
  31. status := cowboy:http_status(),
  32. %% Headers sent with the informational response.
  33. headers := cowboy:http_headers(),
  34. %% Time when the informational response was sent.
  35. time := integer()
  36. }.
  37. -type metrics() :: #{
  38. %% The identifier for this listener.
  39. ref := ranch:ref(),
  40. %% The pid for this connection.
  41. pid := pid(),
  42. %% The streamid also indicates the total number of requests on
  43. %% this connection (StreamID div 2 + 1).
  44. streamid := cowboy_stream:streamid(),
  45. %% The terminate reason is always useful.
  46. reason := cowboy_stream:reason(),
  47. %% A filtered Req object or a partial Req object
  48. %% depending on how far the request got to.
  49. req => cowboy_req:req(),
  50. partial_req => cowboy_stream:partial_req(),
  51. %% Response status.
  52. resp_status := cowboy:http_status(),
  53. %% Filtered response headers.
  54. resp_headers := cowboy:http_headers(),
  55. %% Start/end of the processing of the request.
  56. %%
  57. %% This represents the time from this stream handler's init
  58. %% to terminate. Note that this doesn't indicate the response
  59. %% has been sent fully, it still may be queued up in a buffer.
  60. req_start => integer(),
  61. req_end => integer(),
  62. %% Start/end of the receiving of the request body.
  63. %% Begins when the first packet has been received.
  64. req_body_start => integer(),
  65. req_body_end => integer(),
  66. %% Start/end of the sending of the response.
  67. %% Begins when we send the headers and ends on the final
  68. %% packet of the response body. If everything is sent at
  69. %% once these values are identical.
  70. resp_start => integer(),
  71. resp_end => integer(),
  72. %% For early errors all we get is the time we received it.
  73. early_error_time => integer(),
  74. %% Start/end of spawned processes. This is where most of
  75. %% the user code lies, excluding stream handlers. On a
  76. %% default Cowboy configuration there should be only one
  77. %% process: the request process.
  78. procs => proc_metrics(),
  79. %% Informational responses sent before the final response.
  80. informational => [informational_metrics()],
  81. %% Length of the request and response bodies. This does
  82. %% not include the framing.
  83. req_body_length => non_neg_integer(),
  84. resp_body_length => non_neg_integer()
  85. }.
  86. -export_type([metrics/0]).
  87. -record(state, {
  88. next :: any(),
  89. callback :: fun((metrics()) -> any()),
  90. resp_headers_filter :: undefined | fun((cowboy:http_headers()) -> cowboy:http_headers()),
  91. req :: map(),
  92. resp_status :: undefined | cowboy:http_status(),
  93. resp_headers :: undefined | cowboy:http_headers(),
  94. ref :: ranch:ref(),
  95. req_start :: integer(),
  96. req_end :: undefined | integer(),
  97. req_body_start :: undefined | integer(),
  98. req_body_end :: undefined | integer(),
  99. resp_start :: undefined | integer(),
  100. resp_end :: undefined | integer(),
  101. procs = #{} :: proc_metrics(),
  102. informational = [] :: [informational_metrics()],
  103. req_body_length = 0 :: non_neg_integer(),
  104. resp_body_length = 0 :: non_neg_integer()
  105. }).
  106. -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
  107. -> {[{spawn, pid(), timeout()}], #state{}}.
  108. init(StreamID, Req=#{ref := Ref}, Opts=#{metrics_callback := Fun}) ->
  109. ReqStart = erlang:monotonic_time(),
  110. {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts),
  111. FilteredReq = case maps:get(metrics_req_filter, Opts, undefined) of
  112. undefined -> Req;
  113. ReqFilter -> ReqFilter(Req)
  114. end,
  115. RespHeadersFilter = maps:get(metrics_resp_headers_filter, Opts, undefined),
  116. {Commands, fold(Commands, #state{
  117. next=Next,
  118. callback=Fun,
  119. resp_headers_filter=RespHeadersFilter,
  120. req=FilteredReq,
  121. ref=Ref,
  122. req_start=ReqStart
  123. })}.
  124. -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
  125. -> {cowboy_stream:commands(), State} when State::#state{}.
  126. data(StreamID, IsFin=fin, Data, State=#state{req_body_start=undefined}) ->
  127. ReqBody = erlang:monotonic_time(),
  128. do_data(StreamID, IsFin, Data, State#state{
  129. req_body_start=ReqBody,
  130. req_body_end=ReqBody,
  131. req_body_length=byte_size(Data)
  132. });
  133. data(StreamID, IsFin=fin, Data, State=#state{req_body_length=ReqBodyLen}) ->
  134. ReqBodyEnd = erlang:monotonic_time(),
  135. do_data(StreamID, IsFin, Data, State#state{
  136. req_body_end=ReqBodyEnd,
  137. req_body_length=ReqBodyLen + byte_size(Data)
  138. });
  139. data(StreamID, IsFin, Data, State=#state{req_body_start=undefined}) ->
  140. ReqBodyStart = erlang:monotonic_time(),
  141. do_data(StreamID, IsFin, Data, State#state{
  142. req_body_start=ReqBodyStart,
  143. req_body_length=byte_size(Data)
  144. });
  145. data(StreamID, IsFin, Data, State=#state{req_body_length=ReqBodyLen}) ->
  146. do_data(StreamID, IsFin, Data, State#state{
  147. req_body_length=ReqBodyLen + byte_size(Data)
  148. }).
  149. do_data(StreamID, IsFin, Data, State0=#state{next=Next0}) ->
  150. {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
  151. {Commands, fold(Commands, State0#state{next=Next})}.
  152. -spec info(cowboy_stream:streamid(), any(), State)
  153. -> {cowboy_stream:commands(), State} when State::#state{}.
  154. info(StreamID, Info={'EXIT', Pid, Reason}, State0=#state{procs=Procs}) ->
  155. ProcEnd = erlang:monotonic_time(),
  156. P = maps:get(Pid, Procs),
  157. State = State0#state{procs=Procs#{Pid => P#{
  158. exit => ProcEnd,
  159. reason => Reason
  160. }}},
  161. do_info(StreamID, Info, State);
  162. info(StreamID, Info, State) ->
  163. do_info(StreamID, Info, State).
  164. do_info(StreamID, Info, State0=#state{next=Next0}) ->
  165. {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
  166. {Commands, fold(Commands, State0#state{next=Next})}.
  167. fold([], State) ->
  168. State;
  169. fold([{spawn, Pid, _}|Tail], State0=#state{procs=Procs}) ->
  170. ProcStart = erlang:monotonic_time(),
  171. State = State0#state{procs=Procs#{Pid => #{spawn => ProcStart}}},
  172. fold(Tail, State);
  173. fold([{inform, Status, Headers}|Tail],
  174. State=#state{informational=Infos}) ->
  175. Time = erlang:monotonic_time(),
  176. fold(Tail, State#state{informational=[#{
  177. status => Status,
  178. headers => Headers,
  179. time => Time
  180. }|Infos]});
  181. fold([{response, Status, Headers, Body}|Tail],
  182. State=#state{resp_headers_filter=RespHeadersFilter}) ->
  183. Resp = erlang:monotonic_time(),
  184. fold(Tail, State#state{
  185. resp_status=Status,
  186. resp_headers=case RespHeadersFilter of
  187. undefined -> Headers;
  188. _ -> RespHeadersFilter(Headers)
  189. end,
  190. resp_start=Resp,
  191. resp_end=Resp,
  192. resp_body_length=resp_body_length(Body)
  193. });
  194. fold([{headers, Status, Headers}|Tail],
  195. State=#state{resp_headers_filter=RespHeadersFilter}) ->
  196. RespStart = erlang:monotonic_time(),
  197. fold(Tail, State#state{
  198. resp_status=Status,
  199. resp_headers=case RespHeadersFilter of
  200. undefined -> Headers;
  201. _ -> RespHeadersFilter(Headers)
  202. end,
  203. resp_start=RespStart
  204. });
  205. fold([{data, nofin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) ->
  206. fold(Tail, State#state{
  207. resp_body_length=RespBodyLen + resp_body_length(Data)
  208. });
  209. fold([{data, fin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) ->
  210. RespEnd = erlang:monotonic_time(),
  211. fold(Tail, State#state{
  212. resp_end=RespEnd,
  213. resp_body_length=RespBodyLen + resp_body_length(Data)
  214. });
  215. fold([_|Tail], State) ->
  216. fold(Tail, State).
  217. -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
  218. terminate(StreamID, Reason, #state{next=Next, callback=Fun,
  219. req=Req, resp_status=RespStatus, resp_headers=RespHeaders, ref=Ref,
  220. req_start=ReqStart, req_body_start=ReqBodyStart,
  221. req_body_end=ReqBodyEnd, resp_start=RespStart, resp_end=RespEnd,
  222. procs=Procs, informational=Infos,
  223. req_body_length=ReqBodyLen, resp_body_length=RespBodyLen}) ->
  224. Res = cowboy_stream:terminate(StreamID, Reason, Next),
  225. ReqEnd = erlang:monotonic_time(),
  226. Metrics = #{
  227. ref => Ref,
  228. pid => self(),
  229. streamid => StreamID,
  230. reason => Reason,
  231. req => Req,
  232. resp_status => RespStatus,
  233. resp_headers => RespHeaders,
  234. req_start => ReqStart,
  235. req_end => ReqEnd,
  236. req_body_start => ReqBodyStart,
  237. req_body_end => ReqBodyEnd,
  238. resp_start => RespStart,
  239. resp_end => RespEnd,
  240. procs => Procs,
  241. informational => lists:reverse(Infos),
  242. req_body_length => ReqBodyLen,
  243. resp_body_length => RespBodyLen
  244. },
  245. Fun(Metrics),
  246. Res.
  247. -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
  248. cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
  249. when Resp::cowboy_stream:resp_command().
  250. early_error(StreamID, Reason, PartialReq=#{ref := Ref}, Resp0, Opts=#{metrics_callback := Fun}) ->
  251. Time = erlang:monotonic_time(),
  252. Resp = {response, RespStatus, RespHeaders, RespBody}
  253. = cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp0, Opts),
  254. %% As far as metrics go we are limited in what we can provide
  255. %% in this case.
  256. Metrics = #{
  257. ref => Ref,
  258. pid => self(),
  259. streamid => StreamID,
  260. reason => Reason,
  261. partial_req => PartialReq,
  262. resp_status => RespStatus,
  263. resp_headers => RespHeaders,
  264. early_error_time => Time,
  265. resp_body_length => resp_body_length(RespBody)
  266. },
  267. Fun(Metrics),
  268. Resp.
  269. resp_body_length({sendfile, _, Len, _}) ->
  270. Len;
  271. resp_body_length(Data) ->
  272. iolist_size(Data).