diff options
-rw-r--r-- | ebin/cowboy.app | 2 | ||||
-rw-r--r-- | src/cowboy_tracer_h.erl | 172 | ||||
-rw-r--r-- | test/cowboy_test.erl | 6 | ||||
-rw-r--r-- | test/tracer_SUITE.erl | 423 |
4 files changed, 599 insertions, 4 deletions
diff --git a/ebin/cowboy.app b/ebin/cowboy.app index 356628d..8efc8af 100644 --- a/ebin/cowboy.app +++ b/ebin/cowboy.app @@ -1,7 +1,7 @@ {application, 'cowboy', [ {description, "Small, fast, modern HTTP server."}, {vsn, "2.0.0"}, - {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_iolists','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_websocket']}, + {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_iolists','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']}, {registered, [cowboy_sup,cowboy_clock]}, {applications, [kernel,stdlib,crypto,cowlib,ranch]}, {mod, {cowboy_app, []}}, diff --git a/src/cowboy_tracer_h.erl b/src/cowboy_tracer_h.erl new file mode 100644 index 0000000..1f054e3 --- /dev/null +++ b/src/cowboy_tracer_h.erl @@ -0,0 +1,172 @@ +%% Copyright (c) 2017, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_tracer_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-export([tracer_process/3]). +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type match_predicate() + :: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()). + +-type tracer_match_specs() :: [match_predicate() + | {method, binary()} + | {host, binary()} + | {path, binary()} + | {path_start, binary()} + | {header, binary()} + | {header, binary(), binary()} + | {peer_ip, inet:ip_address()} +]. +-export_type([tracer_match_specs/0]). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), any()}. +init(StreamID, Req, Opts) -> + Result = init_tracer(StreamID, Req, Opts), + {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), + case Result of + no_tracing -> {Commands, Next}; + {tracing, TracerPid} -> {[{spawn, TracerPid, 5000}|Commands], Next} + end. + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::any(). +data(StreamID, IsFin, Data, Next) -> + cowboy_stream:data(StreamID, IsFin, Data, Next). + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::any(). +info(StreamID, Info, Next) -> + cowboy_stream:info(StreamID, Info, Next). + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), any()) -> any(). +terminate(StreamID, Reason, Next) -> + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). + +%% Internal. + +init_tracer(StreamID, Req, Opts=#{tracer_match_specs := List, tracer_callback := _}) -> + case match(List, StreamID, Req, Opts) of + false -> + no_tracing; + true -> + start_tracer(StreamID, Req, Opts) + end; +%% When the options tracer_match_specs or tracer_callback +%% arenot provided we do not enable tracing. +init_tracer(_, _, _) -> + no_tracing. + +match([], _, _, _) -> + true; +match([Predicate|Tail], StreamID, Req, Opts) when is_function(Predicate) -> + case Predicate(StreamID, Req, Opts) of + true -> match(Tail, StreamID, Req, Opts); + false -> false + end; +match([{method, Value}|Tail], StreamID, Req=#{method := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{host, Value}|Tail], StreamID, Req=#{host := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{path, Value}|Tail], StreamID, Req=#{path := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{path_start, PathStart}|Tail], StreamID, Req=#{path := Path}, Opts) -> + Len = byte_size(PathStart), + case Path of + <<PathStart:Len/binary, _/bits>> -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{header, Name}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> + case Headers of + #{Name := _} -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{header, Name, Value}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> + case Headers of + #{Name := Value} -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{peer_ip, IP}|Tail], StreamID, Req=#{peer := {IP, _}}, Opts) -> + match(Tail, StreamID, Req, Opts); +match(_, _, _, _) -> + false. + +%% We only start the tracer if one wasn't started before. +start_tracer(StreamID, Req, Opts) -> + case erlang:trace_info(self(), tracer) of + {tracer, []} -> + TracerPid = proc_lib:spawn_link(?MODULE, tracer_process, [StreamID, Req, Opts]), + erlang:trace_pattern({'_', '_', '_'}, [{'_', [], [{return_trace}]}], [local]), + erlang:trace_pattern(on_load, [{'_', [], [{return_trace}]}], [local]), + erlang:trace(self(), true, [ + send, 'receive', call, return_to, procs, ports, + monotonic_timestamp, set_on_spawn, {tracer, TracerPid} + ]), + {tracing, TracerPid}; + _ -> + no_tracing + end. + +%% Tracer process. + +-spec tracer_process(_, _, _) -> no_return(). +tracer_process(StreamID, Req=#{pid := Parent}, Opts=#{tracer_callback := Fun}) -> + process_flag(trap_exit, true), + State = Fun(init, {StreamID, Req, Opts}), + tracer_loop(Parent, Fun, State). + +tracer_loop(Parent, Fun, State) -> + receive + Msg when element(1, Msg) =:= trace_ts -> + Fun(Msg, State), + tracer_loop(Parent, Fun, State); + {'EXIT', Parent, Reason} -> + exit(Reason); + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Fun, State}); + Msg -> + error_logger:error_msg("~p: Tracer process received stray message ~9999p~n", + [?MODULE, Msg]), + tracer_loop(Parent, Fun, State) + end. + +%% System callbacks. + +-spec system_continue(pid(), _, {fun(), any()}) -> no_return(). +system_continue(Parent, _, {Fun, State}) -> + tracer_loop(Parent, Fun, State). + +-spec system_terminate(any(), _, _, _) -> no_return(). +system_terminate(Reason, _, _, _) -> + exit(Reason). + +-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::any(). +system_code_change(Misc, _, _, _) -> + {ok, Misc}. diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl index 31cba63..bdeb801 100644 --- a/test/cowboy_test.erl +++ b/test/cowboy_test.erl @@ -22,19 +22,19 @@ init_http(Ref, ProtoOpts, Config) -> {ok, _} = cowboy:start_clear(Ref, [{port, 0}], ProtoOpts), Port = ranch:get_port(Ref), - [{type, tcp}, {protocol, http}, {port, Port}, {opts, []}|Config]. + [{ref, Ref}, {type, tcp}, {protocol, http}, {port, Port}, {opts, []}|Config]. init_https(Ref, ProtoOpts, Config) -> Opts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(Ref, Opts ++ [{port, 0}], ProtoOpts), Port = ranch:get_port(Ref), - [{type, ssl}, {protocol, http}, {port, Port}, {opts, Opts}|Config]. + [{ref, Ref}, {type, ssl}, {protocol, http}, {port, Port}, {opts, Opts}|Config]. init_http2(Ref, ProtoOpts, Config) -> Opts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(Ref, Opts ++ [{port, 0}], ProtoOpts), Port = ranch:get_port(Ref), - [{type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config]. + [{ref, Ref}, {type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config]. %% Common group of listeners used by most suites. diff --git a/test/tracer_SUITE.erl b/test/tracer_SUITE.erl new file mode 100644 index 0000000..40ede6c --- /dev/null +++ b/test/tracer_SUITE.erl @@ -0,0 +1,423 @@ +%% Copyright (c) 2017, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(tracer_SUITE). +-compile(export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). +-import(cowboy_test, [gun_open/1]). +-import(cowboy_test, [gun_down/1]). + +%% ct. + +all() -> + cowboy_test:common_all(). + +%% We want tests for each group to execute sequentially +%% because we need to modify the protocol options. Groups +%% can run in parallel however. +groups() -> + Tests = ct_helper:all(?MODULE), + [ + {http, [], Tests}, + {https, [], Tests}, + {h2, [], Tests}, + {h2c, [], Tests}, + {http_compress, [], Tests}, + {https_compress, [], Tests}, + {h2_compress, [], Tests}, + {h2c_compress, [], Tests} + ]. + +init_per_group(Name = http, Config) -> + cowboy_test:init_http(Name, init_plain_opts(Config), Config); +init_per_group(Name = https, Config) -> + cowboy_test:init_http(Name, init_plain_opts(Config), Config); +init_per_group(Name = h2, Config) -> + cowboy_test:init_http2(Name, init_plain_opts(Config), Config); +init_per_group(Name = h2c, Config) -> + Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), + lists:keyreplace(protocol, 1, Config1, {protocol, http2}); +init_per_group(Name = http_compress, Config) -> + cowboy_test:init_http(Name, init_compress_opts(Config), Config); +init_per_group(Name = https_compress, Config) -> + cowboy_test:init_http(Name, init_compress_opts(Config), Config); +init_per_group(Name = h2_compress, Config) -> + cowboy_test:init_http2(Name, init_compress_opts(Config), Config); +init_per_group(Name = h2c_compress, Config) -> + Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), + lists:keyreplace(protocol, 1, Config1, {protocol, http2}). + +end_per_group(Name, _) -> + cowboy:stop_listener(Name). + +init_plain_opts(Config) -> + #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + stream_handlers => [cowboy_tracer_h, cowboy_stream_h] + }. + +init_compress_opts(Config) -> + #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + stream_handlers => [cowboy_tracer_h, cowboy_compress_h, cowboy_stream_h] + }. + +init_routes(_) -> [ + {"localhost", [ + {"/", hello_h, []}, + {"/longer/hello/path", hello_h, []} + ]} +]. + +do_get(Path, Config) -> + %% Perform a GET request. + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, Path, [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"x-test-pid">>, pid_to_list(self())} + ]), + {response, nofin, 200, _Headers} = gun:await(ConnPid, Ref), + {ok, _Body} = gun:await_body(ConnPid, Ref), + gun:close(ConnPid). + +%% We only care about cowboy_req:reply/4 calls. +do_tracer_callback(Pid) -> + fun + (init, _) -> undefined; + (Event={trace_ts, _, call, {cowboy_req, reply, _}, _}, State) -> Pid ! Event, State; + (_, State) -> State + end. + +%% Tests. + +predicate_true(Config) -> + doc("Predicate function returns true, unconditionally enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [fun(_,_,_) -> true end] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +predicate_false(Config) -> + doc("Predicate function returns false, unconditionally disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [fun(_,_,_) -> false end] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +method(Config) -> + doc("Method is the same as the request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{method, <<"GET">>}] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +method_no_match(Config) -> + doc("Method is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{method, <<"POST">>}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +host(Config) -> + doc("Host is the same as the request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{host, <<"localhost">>}] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +host_no_match(Config) -> + doc("Host is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{host, <<"ninenines.eu">>}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +path(Config) -> + doc("Path is the same as the request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{path, <<"/longer/hello/path">>}] + }), + do_get("/longer/hello/path", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +path_no_match(Config) -> + doc("Path is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{path, <<"/some/other/path">>}] + }), + do_get("/longer/hello/path", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +path_start(Config) -> + doc("Start of path is the same as request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{path_start, <<"/longer/hello">>}] + }), + do_get("/longer/hello/path", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +path_start_no_match(Config) -> + doc("Start of path is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{path_start, <<"/shorter/hello">>}] + }), + do_get("/longer/hello/path", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +header_defined(Config) -> + doc("Header is defined in the request, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{header, <<"accept-encoding">>}] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +header_defined_no_match(Config) -> + doc("Header is not defined in the request, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{header, <<"accept-language">>}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +header_value(Config) -> + doc("Header value is the same as the request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{header, <<"accept-encoding">>, <<"gzip">>}] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +header_value_no_match(Config) -> + doc("Header value is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{header, <<"accept-encoding">>, <<"nope">>}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +peer_ip(Config) -> + doc("Peer IP is the same as the request's, enable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{peer_ip, {127, 0, 0, 1}}] + }), + do_get("/", Config), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end. + +peer_ip_no_match(Config) -> + doc("Peer IP is different from the request's, disable tracing."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [{peer_ip, {8, 8, 8, 8}}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +missing_callback(Config) -> + doc("Ensure the request is still processed if the callback is not provided."), + Ref = config(ref, Config), + Opts0 = ranch:get_protocol_options(Ref), + Opts = maps:remove(tracer_callback, Opts0), + ranch:set_protocol_options(Ref, Opts#{ + tracer_match_specs => [{method, <<"GET">>}] + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +missing_match_specs(Config) -> + doc("Ensure the request is still processed if match specs are not provided."), + Ref = config(ref, Config), + Opts0 = ranch:get_protocol_options(Ref), + Opts = maps:remove(tracer_match_specs, Opts0), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()) + }), + do_get("/", Config), + receive + Msg when element(1, Msg) =:= trace_ts -> + error(Msg) + after 100 -> + ok + end. + +two_matching_requests(Config) -> + doc("Perform two requests that enable tracing on the same connection."), + Ref = config(ref, Config), + Opts = ranch:get_protocol_options(Ref), + ranch:set_protocol_options(Ref, Opts#{ + tracer_callback => do_tracer_callback(self()), + tracer_match_specs => [fun(_,_,_) -> true end] + }), + %% Perform a GET request. + ConnPid = gun_open(Config), + Ref1 = gun:get(ConnPid, "/", []), + {response, nofin, 200, _} = gun:await(ConnPid, Ref1), + {ok, _} = gun:await_body(ConnPid, Ref1), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end, + %% Perform a second GET request on the same connection. + Ref2 = gun:get(ConnPid, "/", []), + {response, nofin, 200, _} = gun:await(ConnPid, Ref2), + {ok, _} = gun:await_body(ConnPid, Ref2), + receive + {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> + ok + after 100 -> + error(timeout) + end, + gun:close(ConnPid). |