metrics_SUITE.erl 17 KB

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