metrics_SUITE.erl 16 KB

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