metrics_SUITE.erl 17 KB

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