metrics_SUITE.erl 16 KB

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