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
15-module(cowboy_tracer_h).
16-behavior(cowboy_stream).
17
18-export([init/3]).
19-export([data/4]).
20-export([info/3]).
21-export([terminate/3]).
22-export([early_error/5]).
23
24-export([set_trace_patterns/0]).
25
26-export([tracer_process/3]).
27-export([system_continue/3]).
28-export([system_terminate/4]).
29-export([system_code_change/4]).
30
31-type match_predicate()
32	:: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()).
33
34-type tracer_match_specs() :: [match_predicate()
35	| {method, binary()}
36	| {host, binary()}
37	| {path, binary()}
38	| {path_start, binary()}
39	| {header, binary()}
40	| {header, binary(), binary()}
41	| {peer_ip, inet:ip_address()}
42].
43-export_type([tracer_match_specs/0]).
44
45-type tracer_callback() :: fun((init | terminate | tuple(), any()) -> any()).
46-export_type([tracer_callback/0]).
47
48-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
49	-> {cowboy_stream:commands(), any()}.
50init(StreamID, Req, Opts) ->
51	init_tracer(StreamID, Req, Opts),
52	cowboy_stream:init(StreamID, Req, Opts).
53
54-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
55	-> {cowboy_stream:commands(), State} when State::any().
56data(StreamID, IsFin, Data, Next) ->
57	cowboy_stream:data(StreamID, IsFin, Data, Next).
58
59-spec info(cowboy_stream:streamid(), any(), State)
60	-> {cowboy_stream:commands(), State} when State::any().
61info(StreamID, Info, Next) ->
62	cowboy_stream:info(StreamID, Info, Next).
63
64-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), any()) -> any().
65terminate(StreamID, Reason, Next) ->
66	cowboy_stream:terminate(StreamID, Reason, Next).
67
68-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
69	cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
70	when Resp::cowboy_stream:resp_command().
71early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
72	cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
73
74%% API.
75
76%% These trace patterns are most likely not suitable for production.
77-spec set_trace_patterns() -> ok.
78set_trace_patterns() ->
79	erlang:trace_pattern({'_', '_', '_'}, [{'_', [], [{return_trace}]}], [local]),
80	erlang:trace_pattern(on_load, [{'_', [], [{return_trace}]}], [local]),
81	ok.
82
83%% Internal.
84
85init_tracer(StreamID, Req, Opts=#{tracer_match_specs := List, tracer_callback := _}) ->
86	case match(List, StreamID, Req, Opts) of
87		false ->
88			ok;
89		true ->
90			start_tracer(StreamID, Req, Opts)
91	end;
92%% When the options tracer_match_specs or tracer_callback
93%% are not provided we do not enable tracing.
94init_tracer(_, _, _) ->
95	ok.
96
97match([], _, _, _) ->
98	true;
99match([Predicate|Tail], StreamID, Req, Opts) when is_function(Predicate) ->
100	case Predicate(StreamID, Req, Opts) of
101		true -> match(Tail, StreamID, Req, Opts);
102		false -> false
103	end;
104match([{method, Value}|Tail], StreamID, Req=#{method := Value}, Opts) ->
105	match(Tail, StreamID, Req, Opts);
106match([{host, Value}|Tail], StreamID, Req=#{host := Value}, Opts) ->
107	match(Tail, StreamID, Req, Opts);
108match([{path, Value}|Tail], StreamID, Req=#{path := Value}, Opts) ->
109	match(Tail, StreamID, Req, Opts);
110match([{path_start, PathStart}|Tail], StreamID, Req=#{path := Path}, Opts) ->
111	Len = byte_size(PathStart),
112	case Path of
113		<<PathStart:Len/binary, _/bits>> -> match(Tail, StreamID, Req, Opts);
114		_ -> false
115	end;
116match([{header, Name}|Tail], StreamID, Req=#{headers := Headers}, Opts) ->
117	case Headers of
118		#{Name := _} -> match(Tail, StreamID, Req, Opts);
119		_ -> false
120	end;
121match([{header, Name, Value}|Tail], StreamID, Req=#{headers := Headers}, Opts) ->
122	case Headers of
123		#{Name := Value} -> match(Tail, StreamID, Req, Opts);
124		_ -> false
125	end;
126match([{peer_ip, IP}|Tail], StreamID, Req=#{peer := {IP, _}}, Opts) ->
127	match(Tail, StreamID, Req, Opts);
128match(_, _, _, _) ->
129	false.
130
131%% We only start the tracer if one wasn't started before.
132start_tracer(StreamID, Req, Opts) ->
133	case erlang:trace_info(self(), tracer) of
134		{tracer, []} ->
135			TracerPid = proc_lib:spawn_link(?MODULE, tracer_process, [StreamID, Req, Opts]),
136			%% The default flags are probably not suitable for production.
137			Flags = maps:get(tracer_flags, Opts, [
138				send, 'receive', call, return_to,
139				procs, ports, monotonic_timestamp,
140				%% The set_on_spawn flag is necessary to catch events
141				%% from request processes.
142				set_on_spawn
143			]),
144			erlang:trace(self(), true, [{tracer, TracerPid}|Flags]),
145			ok;
146		_ ->
147			ok
148	end.
149
150%% Tracer process.
151
152-spec tracer_process(_, _, _) -> no_return().
153tracer_process(StreamID, Req=#{pid := Parent}, Opts=#{tracer_callback := Fun}) ->
154	%% This is necessary because otherwise the tracer could stop
155	%% before it has finished processing the events in its queue.
156	process_flag(trap_exit, true),
157	State = Fun(init, {StreamID, Req, Opts}),
158	tracer_loop(Parent, Opts, State).
159
160tracer_loop(Parent, Opts=#{tracer_callback := Fun}, State0) ->
161	receive
162		Msg when element(1, Msg) =:= trace_ts ->
163			State = Fun(Msg, State0),
164			tracer_loop(Parent, Opts, State);
165		{'EXIT', Parent, Reason} ->
166			tracer_terminate(Reason, Opts, State0);
167		{system, From, Request} ->
168			sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Opts, State0});
169		Msg ->
170			cowboy:log(warning, "~p: Tracer process received stray message ~9999p~n",
171				[?MODULE, Msg], Opts),
172			tracer_loop(Parent, Opts, State0)
173	end.
174
175-spec tracer_terminate(_, _, _) -> no_return().
176tracer_terminate(Reason, #{tracer_callback := Fun}, State) ->
177	_ = Fun(terminate, State),
178	exit(Reason).
179
180%% System callbacks.
181
182-spec system_continue(pid(), _, {cowboy:opts(), any()}) -> no_return().
183system_continue(Parent, _, {Opts, State}) ->
184	tracer_loop(Parent, Opts, State).
185
186-spec system_terminate(any(), _, _, _) -> no_return().
187system_terminate(Reason, _, _, {Opts, State}) ->
188	tracer_terminate(Reason, Opts, State).
189
190-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::any().
191system_code_change(Misc, _, _, _) ->
192	{ok, Misc}.
193