metrics_SUITE.erl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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(metrics_SUITE).
  15. -compile(export_all).
  16. -import(ct_helper, [config/2]).
  17. -import(ct_helper, [doc/1]).
  18. -import(cowboy_test, [gun_open/1]).
  19. -import(cowboy_test, [gun_down/1]).
  20. -import(cowboy_test, [raw_open/1]).
  21. -import(cowboy_test, [raw_send/2]).
  22. -import(cowboy_test, [raw_recv_head/1]).
  23. %% ct.
  24. all() ->
  25. cowboy_test:common_all().
  26. groups() ->
  27. cowboy_test:common_groups(ct_helper:all(?MODULE)).
  28. init_per_group(Name = http, Config) ->
  29. cowboy_test:init_http(Name, init_plain_opts(Config), Config);
  30. init_per_group(Name = https, Config) ->
  31. cowboy_test:init_http(Name, init_plain_opts(Config), Config);
  32. init_per_group(Name = h2, Config) ->
  33. cowboy_test:init_http2(Name, init_plain_opts(Config), Config);
  34. init_per_group(Name = h2c, Config) ->
  35. Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
  36. lists:keyreplace(protocol, 1, Config1, {protocol, http2});
  37. init_per_group(Name = http_compress, Config) ->
  38. cowboy_test:init_http(Name, init_compress_opts(Config), Config);
  39. init_per_group(Name = https_compress, Config) ->
  40. cowboy_test:init_http(Name, init_compress_opts(Config), Config);
  41. init_per_group(Name = h2_compress, Config) ->
  42. cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
  43. init_per_group(Name = h2c_compress, Config) ->
  44. Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
  45. lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
  46. end_per_group(Name, _) ->
  47. cowboy:stop_listener(Name).
  48. init_plain_opts(Config) ->
  49. #{
  50. env => #{dispatch => cowboy_router:compile(init_routes(Config))},
  51. metrics_callback => do_metrics_callback(),
  52. stream_handlers => [cowboy_metrics_h, cowboy_stream_h]
  53. }.
  54. init_compress_opts(Config) ->
  55. #{
  56. env => #{dispatch => cowboy_router:compile(init_routes(Config))},
  57. metrics_callback => do_metrics_callback(),
  58. stream_handlers => [cowboy_metrics_h, cowboy_compress_h, cowboy_stream_h]
  59. }.
  60. init_routes(_) -> [
  61. {"localhost", [
  62. {"/", hello_h, []},
  63. {"/default", default_h, []},
  64. {"/full/:key", echo_h, []},
  65. {"/resp/:key[/:arg]", resp_h, []},
  66. {"/ws_echo", ws_echo, []}
  67. ]}
  68. ].
  69. do_metrics_callback() ->
  70. fun(Metrics) ->
  71. Pid = case Metrics of
  72. #{req := #{headers := #{<<"x-test-pid">> := P}}} ->
  73. list_to_pid(binary_to_list(P));
  74. #{partial_req := #{headers := #{<<"x-test-pid">> := P}}} ->
  75. list_to_pid(binary_to_list(P));
  76. _ ->
  77. whereis(early_error_metrics)
  78. end,
  79. Pid ! {metrics, self(), Metrics},
  80. ok
  81. end.
  82. %% Tests.
  83. hello_world(Config) ->
  84. doc("Confirm metrics are correct for a normal GET request."),
  85. do_get("/", Config).
  86. do_get(Path, Config) ->
  87. %% Perform a GET request.
  88. ConnPid = gun_open(Config),
  89. Ref = gun:get(ConnPid, Path, [
  90. {<<"accept-encoding">>, <<"gzip">>},
  91. {<<"x-test-pid">>, pid_to_list(self())}
  92. ]),
  93. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
  94. {ok, RespBody} = gun:await_body(ConnPid, Ref),
  95. gun:close(ConnPid),
  96. %% Receive the metrics and validate them.
  97. receive
  98. {metrics, From, Metrics} ->
  99. %% Ensure the timestamps are in the expected order.
  100. #{
  101. req_start := ReqStart, req_end := ReqEnd,
  102. resp_start := RespStart, resp_end := RespEnd
  103. } = Metrics,
  104. true = (ReqStart =< RespStart)
  105. and (RespStart =< RespEnd)
  106. and (RespEnd =< ReqEnd),
  107. %% We didn't send a body.
  108. #{
  109. req_body_start := undefined,
  110. req_body_end := undefined,
  111. req_body_length := 0
  112. } = Metrics,
  113. %% We got a 200 response with a body.
  114. #{
  115. resp_status := 200,
  116. resp_headers := ExpectedRespHeaders,
  117. resp_body_length := RespBodyLen
  118. } = Metrics,
  119. %% The transfer-encoding header is hidden from stream handlers.
  120. ExpectedRespHeaders = maps:remove(<<"transfer-encoding">>,
  121. maps:from_list(RespHeaders)),
  122. true = byte_size(RespBody) > 0,
  123. true = RespBodyLen > 0,
  124. %% The request process executed normally.
  125. #{procs := Procs} = Metrics,
  126. [{_, #{
  127. spawn := ProcSpawn,
  128. exit := ProcExit,
  129. reason := normal
  130. }}] = maps:to_list(Procs),
  131. true = ProcSpawn =< ProcExit,
  132. %% Confirm other metadata are as expected.
  133. #{
  134. ref := _,
  135. pid := From,
  136. streamid := 1,
  137. reason := normal,
  138. req := #{},
  139. informational := []
  140. } = Metrics,
  141. %% All good!
  142. ok
  143. after 1000 ->
  144. error(timeout)
  145. end.
  146. post_body(Config) ->
  147. doc("Confirm metrics are correct for a normal POST request."),
  148. %% Perform a POST request.
  149. ConnPid = gun_open(Config),
  150. Body = <<0:8000000>>,
  151. Ref = gun:post(ConnPid, "/full/read_body", [
  152. {<<"accept-encoding">>, <<"gzip">>},
  153. {<<"x-test-pid">>, pid_to_list(self())}
  154. ], Body),
  155. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
  156. {ok, RespBody} = gun:await_body(ConnPid, Ref),
  157. gun:close(ConnPid),
  158. %% Receive the metrics and validate them.
  159. receive
  160. {metrics, From, Metrics} ->
  161. %% Ensure the timestamps are in the expected order.
  162. #{
  163. req_start := ReqStart, req_end := ReqEnd,
  164. resp_start := RespStart, resp_end := RespEnd
  165. } = Metrics,
  166. true = (ReqStart =< RespStart)
  167. and (RespStart =< RespEnd)
  168. and (RespEnd =< ReqEnd),
  169. %% We didn't send a body.
  170. #{
  171. req_body_start := ReqBodyStart,
  172. req_body_end := ReqBodyEnd,
  173. req_body_length := ReqBodyLen
  174. } = Metrics,
  175. true = ReqBodyStart =< ReqBodyEnd,
  176. ReqBodyLen = byte_size(Body),
  177. %% We got a 200 response with a body.
  178. #{
  179. resp_status := 200,
  180. resp_headers := ExpectedRespHeaders,
  181. resp_body_length := RespBodyLen
  182. } = Metrics,
  183. ExpectedRespHeaders = maps:from_list(RespHeaders),
  184. true = byte_size(RespBody) > 0,
  185. true = RespBodyLen > 0,
  186. %% The request process executed normally.
  187. #{procs := Procs} = Metrics,
  188. [{_, #{
  189. spawn := ProcSpawn,
  190. exit := ProcExit,
  191. reason := normal
  192. }}] = maps:to_list(Procs),
  193. true = ProcSpawn =< ProcExit,
  194. %% Confirm other metadata are as expected.
  195. #{
  196. ref := _,
  197. pid := From,
  198. streamid := 1,
  199. reason := normal,
  200. req := #{},
  201. informational := []
  202. } = Metrics,
  203. %% All good!
  204. ok
  205. after 1000 ->
  206. error(timeout)
  207. end.
  208. no_resp_body(Config) ->
  209. doc("Confirm metrics are correct for a default 204 response to a GET request."),
  210. %% Perform a GET request.
  211. ConnPid = gun_open(Config),
  212. Ref = gun:get(ConnPid, "/default", [
  213. {<<"accept-encoding">>, <<"gzip">>},
  214. {<<"x-test-pid">>, pid_to_list(self())}
  215. ]),
  216. {response, fin, 204, RespHeaders} = gun:await(ConnPid, Ref),
  217. gun:close(ConnPid),
  218. %% Receive the metrics and validate them.
  219. receive
  220. {metrics, From, Metrics} ->
  221. %% Ensure the timestamps are in the expected order.
  222. #{
  223. req_start := ReqStart, req_end := ReqEnd,
  224. resp_start := RespStart, resp_end := RespEnd
  225. } = Metrics,
  226. true = (ReqStart =< RespStart)
  227. and (RespStart =< RespEnd)
  228. and (RespEnd =< ReqEnd),
  229. %% We didn't send a body.
  230. #{
  231. req_body_start := undefined,
  232. req_body_end := undefined,
  233. req_body_length := 0
  234. } = Metrics,
  235. %% We got a 200 response with a body.
  236. #{
  237. resp_status := 204,
  238. resp_headers := ExpectedRespHeaders,
  239. resp_body_length := 0
  240. } = Metrics,
  241. ExpectedRespHeaders = maps:from_list(RespHeaders),
  242. %% The request process executed normally.
  243. #{procs := Procs} = Metrics,
  244. [{_, #{
  245. spawn := ProcSpawn,
  246. exit := ProcExit,
  247. reason := normal
  248. }}] = maps:to_list(Procs),
  249. true = ProcSpawn =< ProcExit,
  250. %% Confirm other metadata are as expected.
  251. #{
  252. ref := _,
  253. pid := From,
  254. streamid := 1,
  255. reason := normal,
  256. req := #{},
  257. informational := []
  258. } = Metrics,
  259. %% All good!
  260. ok
  261. after 1000 ->
  262. error(timeout)
  263. end.
  264. early_error(Config) ->
  265. case config(protocol, Config) of
  266. http -> do_early_error(Config);
  267. http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
  268. end.
  269. do_early_error(Config) ->
  270. doc("Confirm metrics are correct for an early_error response."),
  271. %% Perform a malformed GET request.
  272. ConnPid = gun_open(Config),
  273. Ref = gun:get(ConnPid, "/", [
  274. {<<"accept-encoding">>, <<"gzip">>},
  275. {<<"host">>, <<"host:port">>},
  276. {<<"x-test-pid">>, pid_to_list(self())}
  277. ]),
  278. {response, fin, 400, RespHeaders} = gun:await(ConnPid, Ref),
  279. gun:close(ConnPid),
  280. %% Receive the metrics and validate them.
  281. receive
  282. {metrics, From, Metrics} ->
  283. %% Confirm the metadata is there as expected.
  284. #{
  285. ref := _,
  286. pid := From,
  287. streamid := 1,
  288. reason := {stream_error, 1, protocol_error, _},
  289. partial_req := #{},
  290. resp_status := 400,
  291. resp_headers := ExpectedRespHeaders,
  292. early_error_time := _,
  293. resp_body_length := 0
  294. } = Metrics,
  295. ExpectedRespHeaders = maps:from_list(RespHeaders),
  296. %% All good!
  297. ok
  298. after 1000 ->
  299. error(timeout)
  300. end.
  301. early_error_request_line(Config) ->
  302. case config(protocol, Config) of
  303. http -> do_early_error_request_line(Config);
  304. http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
  305. end.
  306. do_early_error_request_line(Config) ->
  307. doc("Confirm metrics are correct for an early_error response "
  308. "that occurred on the request-line."),
  309. %% Register the process in order to receive the metrics event.
  310. register(early_error_metrics, self()),
  311. %% Send a malformed request-line.
  312. Client = raw_open(Config),
  313. ok = raw_send(Client, <<"FOO bar\r\n">>),
  314. {'HTTP/1.1', 400, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)),
  315. {RespHeaders, _} = cow_http:parse_headers(Rest),
  316. %% Receive the metrics and validate them.
  317. receive
  318. {metrics, From, Metrics} ->
  319. %% Confirm the metadata is there as expected.
  320. #{
  321. ref := _,
  322. pid := From,
  323. streamid := 1,
  324. reason := {connection_error, protocol_error, _},
  325. partial_req := #{},
  326. resp_status := 400,
  327. resp_headers := ExpectedRespHeaders,
  328. early_error_time := _,
  329. resp_body_length := 0
  330. } = Metrics,
  331. ExpectedRespHeaders = maps:from_list(RespHeaders),
  332. %% All good!
  333. ok
  334. after 1000 ->
  335. error(timeout)
  336. end.
  337. %% This test is identical to normal GET except for the handler.
  338. stream_reply(Config) ->
  339. doc("Confirm metrics are correct for long polling."),
  340. do_get("/resp/stream_reply2/200", Config).
  341. ws(Config) ->
  342. case config(protocol, Config) of
  343. http -> do_ws(Config);
  344. http2 -> doc("It is not currently possible to switch to Websocket over HTTP/2.")
  345. end.
  346. do_ws(Config) ->
  347. doc("Confirm metrics are correct when switching to Websocket."),
  348. ConnPid = gun_open(Config),
  349. {ok, http} = gun:await_up(ConnPid),
  350. gun:ws_upgrade(ConnPid, "/ws_echo", [
  351. {<<"accept-encoding">>, <<"gzip">>},
  352. {<<"x-test-pid">>, pid_to_list(self())}
  353. ]),
  354. receive
  355. {metrics, From, Metrics} ->
  356. %% Ensure the timestamps are in the expected order.
  357. #{
  358. req_start := ReqStart,
  359. req_end := ReqEnd
  360. } = Metrics,
  361. true = ReqStart =< ReqEnd,
  362. %% We didn't send a body.
  363. #{
  364. req_body_start := undefined,
  365. req_body_end := undefined,
  366. req_body_length := 0
  367. } = Metrics,
  368. %% We didn't send a response.
  369. #{
  370. resp_start := undefined,
  371. resp_end := undefined,
  372. resp_status := undefined,
  373. resp_headers := undefined,
  374. resp_body_length := 0
  375. } = Metrics,
  376. %% The request process may not have terminated before terminate
  377. %% is called. We therefore only check when it spawned.
  378. #{procs := Procs} = Metrics,
  379. [{_, #{
  380. spawn := _
  381. }}] = maps:to_list(Procs),
  382. %% Confirm other metadata are as expected.
  383. #{
  384. ref := _,
  385. pid := From,
  386. streamid := 1,
  387. reason := switch_protocol,
  388. req := #{},
  389. %% A 101 upgrade response was sent.
  390. informational := [#{
  391. status := 101,
  392. headers := #{
  393. <<"connection">> := <<"Upgrade">>,
  394. <<"upgrade">> := <<"websocket">>,
  395. <<"sec-websocket-accept">> := _
  396. },
  397. time := _
  398. }]
  399. } = Metrics,
  400. %% All good!
  401. ok
  402. after 1000 ->
  403. error(timeout)
  404. end,
  405. %% And of course the upgrade completed successfully after that.
  406. receive
  407. {gun_ws_upgrade, ConnPid, ok, _} ->
  408. ok
  409. after 1000 ->
  410. error(timeout)
  411. end,
  412. gun:close(ConnPid).