metrics_SUITE.erl 16 KB

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