./ct_report/coverage/mongoose_instrument.COVER.html

1 -module(mongoose_instrument).
2
3 -behaviour(gen_server).
4
5 %% API
6 -export([config_spec/0,
7 start_link/0, persist/0,
8 set_up/1, set_up/3,
9 tear_down/1, tear_down/2,
10 span/4, span/5,
11 execute/3]).
12
13 %% Test API
14 -export([add_handler/2, remove_handler/1]).
15
16 %% gen_server callbacks
17 -export([init/1, handle_call/3, handle_cast/2, code_change/3, handle_info/2, terminate/2]).
18
19 -ignore_xref([start_link/0, set_up/3, tear_down/2, span/4, add_handler/2, remove_handler/1]).
20
21 -include("mongoose.hrl").
22 -include("mongoose_config_spec.hrl").
23
24 -type event_name() :: atom().
25 -type labels() :: #{host_type => mongooseim:host_type()}. % to be extended
26 -type label_key() :: host_type. % to be extended
27 -type label_value() :: mongooseim:host_type(). % to be extended
28 -type metrics() :: #{metric_name() => metric_type()}.
29 -type metric_name() :: atom().
30 -type metric_type() :: spiral | histogram. % to be extended
31 -type measurements() :: #{atom() => term()}.
32 -type spec() :: {event_name(), labels(), config()}.
33 -type config() :: #{metrics => metrics()}. % to be extended
34 -type handler_key() :: atom(). % key in the `instrumentation' section of the config file
35 -type handler_fun() :: fun((event_name(), labels(), config(), measurements()) -> any()).
36 -type handlers() :: {[handler_fun()], config()}.
37 -type execution_time() :: integer().
38 -type measure_fun(Result) :: fun((execution_time(), Result) -> measurements()).
39
40 -callback config_spec() -> mongoose_config_spec:config_section().
41 -callback start() -> ok.
42 -callback stop() -> ok.
43 -callback set_up(event_name(), labels(), config()) -> boolean().
44 -callback handle_event(event_name(), labels(), config(), measurements()) -> any().
45
46 -optional_callbacks([config_spec/0, start/0, stop/0]).
47
48 -export_type([event_name/0, labels/0, label_key/0, label_value/0, config/0, measurements/0,
49 spec/0, handlers/0, metric_name/0, metric_type/0]).
50
51 %% API
52
53 %% @doc Specifies the `instrumentation' section of the config file
54 -spec config_spec() -> mongoose_config_spec:config_section().
55 config_spec() ->
56 53 Items = [{atom_to_binary(Key), config_spec(Key)} || Key <- all_handler_keys()],
57 53 #section{items = maps:from_list(Items),
58 wrap = global_config,
59 include = always}.
60
61 -spec start_link() -> gen_server:start_ret().
62 start_link() ->
63 53 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
64
65 %% @doc Saves the state to a persistent term, improving performance of `execute' and `span'.
66 %% On the other hand, future calls to `set_up' or `tear_down' will update the persistent term,
67 %% which makes them less performant.
68 %% You should call this function only once - after the initial setup, but before handling any load.
69 -spec persist() -> ok.
70 persist() ->
71 53 gen_server:call(?MODULE, persist).
72
73 %% @doc Sets up instrumentation for multiple events.
74 %% @see set_up/3
75 -spec set_up([spec()]) -> ok.
76 set_up(Specs) ->
77 270 lists:foreach(fun({EventName, Labels, Config}) -> set_up(EventName, Labels, Config) end, Specs).
78
79 %% @doc Tears down instrumentation for multiple events.
80 %% @see tear_down/2
81 -spec tear_down([spec()]) -> ok.
82 tear_down(Specs) ->
83 270 lists:foreach(fun({EventName, Labels, _Config}) -> tear_down(EventName, Labels) end, Specs).
84
85 %% @doc Sets up instrumentation for an event identified by `EventName' and `Labels'
86 %% according to `Config'. Fails if the event is already registered, or if the keys of `Labels'
87 %% are different than for already registered events with `EventName'.
88 -spec set_up(event_name(), labels(), config()) -> ok.
89 set_up(EventName, Labels, Config) ->
90 1518 case gen_server:call(?MODULE, {set_up, EventName, Labels, Config}) of
91 1518 ok -> ok;
92
:-(
{error, ErrorMap} -> error(ErrorMap)
93 end.
94
95 %% @doc Tears down instrumentation for an event identified by `EventName' and `Labels'.
96 %% This operation is idempotent.
97 -spec tear_down(event_name(), labels()) -> ok.
98 tear_down(EventName, Labels) ->
99 1518 gen_server:call(?MODULE, {tear_down, EventName, Labels}).
100
101 %% @doc Calls `F', measuring its result with `MeasureF', and calls attached event handlers.
102 %% @see span/5
103 -spec span(event_name(), labels(), fun(() -> Result), measure_fun(Result)) -> Result.
104 span(EventName, Labels, F, MeasureF) ->
105
:-(
span(EventName, Labels, F, [], MeasureF).
106
107 %% @doc Calls `F' with `Args', measuring its execution time.
108 %% The time and the result are passed to `MeasureF', which returns measurements.
109 %% The measurements are then passed to all handlers attached to
110 %% the event identified by `EventName' and `Labels'.
111 %% Fails without calling `F' if the event is not registered.
112 -spec span(event_name(), labels(), fun((...) -> Result), list(), measure_fun(Result)) -> Result.
113 span(EventName, Labels, F, Args, MeasureF) ->
114 13367 Handlers = get_handlers(EventName, Labels),
115 13367 {Time, Result} = timer:tc(F, Args),
116 13367 handle_event(EventName, Labels, MeasureF(Time, Result), Handlers),
117 13367 Result.
118
119 %% @doc Executes all handlers attached to the event identified by `EventName' and `Labels',
120 %% passing `Measurements' to them. Fails if the event is not registered.
121 -spec execute(event_name(), labels(), measurements()) -> ok.
122 execute(EventName, Labels, Measurements) ->
123 1665 Handlers = get_handlers(EventName, Labels),
124 1664 handle_event(EventName, Labels, Measurements, Handlers).
125
126 %% Test API
127
128 -spec add_handler(handler_key(), mongoose_config:value()) -> ok.
129 add_handler(Key, ConfigVal) ->
130 1 case gen_server:call(?MODULE, {add_handler, Key, ConfigVal}) of
131 1 ok -> ok;
132
:-(
{error, ErrorMap} -> error(ErrorMap)
133 end.
134
135 -spec remove_handler(handler_key()) -> ok.
136 remove_handler(Key) ->
137 1 case gen_server:call(?MODULE, {remove_handler, Key}) of
138 1 ok -> ok;
139
:-(
{error, ErrorMap} -> error(ErrorMap)
140 end.
141
142 %% gen_server callbacks
143
144 -type state() :: #{event_name() => #{labels() => handlers()}}.
145
146 -spec init([]) -> {ok, state()}.
147 init([]) ->
148 53 lists:foreach(fun start_handler/1, handler_modules()),
149 53 erlang:process_flag(trap_exit, true), % Make sure that terminate is called
150 53 persistent_term:erase(?MODULE), % Prevent inconsistency when restarted after a kill
151 53 {ok, #{}}.
152
153 -spec handle_call(any(), gen_server:from(), state()) ->
154 {reply, ok | {ok, handlers()} | {error, map()}, state()}.
155 handle_call({set_up, EventName, Labels, Config}, _From, State) ->
156 1518 case set_up_and_register(EventName, Labels, Config, State) of
157 {error, _} = Error ->
158
:-(
{reply, Error, State};
159 NewState = #{} ->
160 1518 update_if_persisted(State, NewState),
161 1518 {reply, ok, NewState}
162 end;
163 handle_call({tear_down, EventName, Labels}, _From, State) ->
164 1518 NewState = deregister(EventName, Labels, State),
165 1518 update_if_persisted(State, NewState),
166 1518 {reply, ok, NewState};
167 handle_call({add_handler, Key, ConfigOpts}, _From, State) ->
168 1 case mongoose_config:lookup_opt([instrumentation, Key]) of
169 {error, not_found} ->
170 1 mongoose_config:set_opt([instrumentation, Key], ConfigOpts),
171 1 Module = handler_module(Key),
172 1 start_handler(Module),
173 1 NewState = update_handlers(State, [], [Module]),
174 1 update_if_persisted(State, NewState),
175 1 {reply, ok, NewState};
176 {ok, ExistingConfig} ->
177
:-(
{reply, {error, #{what => handler_already_configured, handler_key => Key,
178 existing_config => ExistingConfig}},
179 State}
180 end;
181 handle_call({remove_handler, Key}, _From, State) ->
182 1 case mongoose_config:lookup_opt([instrumentation, Key]) of
183 {error, not_found} ->
184
:-(
{reply, {error, #{what => handler_not_configured, handler_key => Key}}, State};
185 {ok, _} ->
186 1 mongoose_config:unset_opt([instrumentation, Key]),
187 1 Module = handler_module(Key),
188 1 NewState = update_handlers(State, [Module], []),
189 1 update_if_persisted(State, NewState),
190 1 stop_handler(Module),
191 1 {reply, ok, NewState}
192 end;
193 handle_call(persist, _From, State) ->
194 53 persistent_term:put(?MODULE, State),
195 53 {reply, ok, State};
196 handle_call({lookup, EventName, Labels}, _From, State) ->
197
:-(
{reply, lookup(EventName, Labels, State), State};
198 handle_call(Request, From, State) ->
199
:-(
?UNEXPECTED_CALL(Request, From),
200
:-(
{reply, {error, #{what => unexpected_call, request => Request}}, State}.
201
202 -spec handle_cast(any(), state()) -> {noreply, state()}.
203 handle_cast(Msg, State) ->
204
:-(
?UNEXPECTED_CAST(Msg),
205
:-(
{noreply, State}.
206
207 -spec handle_info(any(), state()) -> {noreply, state()}.
208 handle_info(Info, State) ->
209
:-(
?UNEXPECTED_INFO(Info),
210
:-(
{noreply, State}.
211
212 -spec terminate(any(), state()) -> ok.
213 terminate(_Reason, _State) ->
214 54 persistent_term:erase(?MODULE),
215 54 lists:foreach(fun stop_handler/1, handler_modules()).
216
217 -spec code_change(any(), state(), any()) -> {ok, state()}.
218 code_change(_OldVsn, State, _Extra) ->
219
:-(
{ok, State}.
220
221 %% Internal functions
222
223 -spec update_if_persisted(state(), state()) -> ok.
224 update_if_persisted(State, NewState) ->
225 3038 try persistent_term:get(?MODULE) of
226 3038 State -> persistent_term:put(?MODULE, NewState)
227 catch
228
:-(
error:badarg -> ok
229 end.
230
231 -spec set_up_and_register(event_name(), labels(), config(), state()) -> state() | {error, map()}.
232 set_up_and_register(EventName, Labels, Config, State) ->
233 1518 LabelKeys = label_keys(Labels),
234 1518 case State of
235 #{EventName := #{Labels := _}} ->
236
:-(
{error, #{what => event_already_registered,
237 event_name => EventName, labels => Labels}};
238 #{EventName := HandlerMap} ->
239 56 {ExistingLabels, _, _} = maps:next(maps:iterator(HandlerMap)),
240 56 case label_keys(ExistingLabels) of
241 LabelKeys ->
242 56 Handlers = do_set_up(EventName, Labels, Config),
243 56 State#{EventName := HandlerMap#{Labels => Handlers}};
244 ExistingKeys ->
245
:-(
{error, #{what => inconsistent_labels,
246 event_name => EventName, labels => Labels,
247 existing_label_keys => ExistingKeys}}
248 end;
249 #{} ->
250 1462 Handlers = do_set_up(EventName, Labels, Config),
251 1462 State#{EventName => #{Labels => Handlers}}
252 end.
253
254 -spec do_set_up(event_name(), labels(), config()) -> handlers().
255 do_set_up(EventName, Labels, Config) ->
256 1518 HandlerFuns = set_up_handlers(EventName, Labels, Config, handler_modules()),
257 1518 {HandlerFuns, Config}.
258
259 -spec update_handlers(state(), [module()], [module()]) -> state().
260 update_handlers(State, ToRemove, ToAdd) ->
261 2 maps:map(fun(EventName, HandlerMap) ->
262
:-(
maps:map(fun(Labels, Handlers) ->
263
:-(
update_event_handlers(EventName, Labels, Handlers,
264 ToRemove, ToAdd)
265 end, HandlerMap)
266 end, State).
267
268 -spec update_event_handlers(event_name(), labels(), handlers(), [module()], [module()]) ->
269 handlers().
270 update_event_handlers(EventName, Labels, {HandlerFuns, Config}, ToRemove, ToAdd) ->
271
:-(
FunsToRemove = modules_to_funs(ToRemove),
272
:-(
FunsToAdd = set_up_handlers(EventName, Labels, Config, ToAdd),
273
:-(
HandlerFuns = HandlerFuns -- FunsToAdd, % sanity check to prevent duplicates
274
:-(
{(HandlerFuns -- FunsToRemove) ++ FunsToAdd, Config}.
275
276 -spec set_up_handlers(event_name(), labels(), config(), [module()]) -> [handler_fun()].
277 set_up_handlers(EventName, Labels, Config, Modules) ->
278 1518 UsedModules = lists:filter(fun(Mod) -> Mod:set_up(EventName, Labels, Config) end, Modules),
279 1518 modules_to_funs(UsedModules).
280
281 -spec deregister(event_name(), labels(), state()) -> state().
282 deregister(EventName, Labels, State) ->
283 1518 case State of
284 #{EventName := HandlerMap} ->
285 1518 case maps:remove(Labels, HandlerMap) of
286 Empty when Empty =:= #{} ->
287 1462 maps:remove(EventName, State);
288 NewHandlerMap ->
289 56 State#{EventName := NewHandlerMap}
290 end;
291 #{} ->
292
:-(
State
293 end.
294
295 -spec lookup(event_name(), labels()) -> {ok, handlers()} | {error, map()}.
296 lookup(EventName, Labels) ->
297 15032 try persistent_term:get(?MODULE) of
298 State ->
299 15032 lookup(EventName, Labels, State)
300 catch
301 %% Although persist/0 should be called before handling traffic,
302 %% some instrumented events might happen before that, and they shouldn't fail.
303 error:badarg ->
304
:-(
?LOG_INFO(#{what => mongoose_instrument_lookup_without_persistent_term,
305
:-(
event_name => EventName, labels => Labels}),
306
:-(
gen_server:call(?MODULE, {lookup, EventName, Labels})
307 end.
308
309 -spec lookup(event_name(), labels(), state()) -> {ok, handlers()} | {error, map()}.
310 lookup(EventName, Labels, State) ->
311 15032 case State of
312 #{EventName := #{Labels := Handlers}} ->
313 15031 {ok, Handlers};
314 #{} ->
315 1 {error, #{what => event_not_registered, event_name => EventName, labels => Labels}}
316 end.
317
318 -spec label_keys(labels()) -> [atom()].
319 label_keys(Labels) ->
320 1574 lists:sort(maps:keys(Labels)).
321
322 -spec get_handlers(event_name(), labels()) -> handlers().
323 get_handlers(EventName, Labels) ->
324 15032 case lookup(EventName, Labels) of
325 15031 {ok, Handlers} -> Handlers;
326 1 {error, ErrorMap} -> error(ErrorMap)
327 end.
328
329 -spec handle_event(event_name(), labels(), measurements(), handlers()) -> ok.
330 handle_event(EventName, Labels, Measurements, {EventHandlers, Config}) ->
331 15031 lists:foreach(fun(HandlerFun) ->
332 59129 call_handler(HandlerFun, EventName, Labels, Config, Measurements)
333 end, EventHandlers).
334
335 -spec modules_to_funs([module()]) -> [handler_fun()].
336 modules_to_funs(Modules) ->
337 1518 [fun Module:handle_event/4 || Module <- Modules].
338
339 -spec handler_modules() -> [module()].
340 handler_modules() ->
341 1625 [handler_module(Key) || Key <- maps:keys(mongoose_config:get_opt(instrumentation))].
342
343 -spec handler_module(handler_key()) -> module().
344 handler_module(Key) ->
345 5975 list_to_existing_atom("mongoose_instrument_" ++ atom_to_list(Key)).
346
347 -spec config_spec(handler_key()) -> mongoose_config_spec:config_section().
348 config_spec(Key) ->
349 159 Module = handler_module(Key),
350 159 case mongoose_lib:is_exported(Module, config_spec, 0) of
351 106 true -> Module:config_spec();
352 53 false -> #section{}
353 end.
354
355 -spec all_handler_keys() -> [handler_key()].
356 all_handler_keys() ->
357 53 [prometheus, exometer, log].
358
359 -spec start_handler(module()) -> ok.
360 start_handler(Module) ->
361 160 case mongoose_lib:is_exported(Module, start, 0) of
362 54 true -> Module:start();
363 106 false -> ok
364 end.
365
366 -spec stop_handler(module()) -> ok.
367 stop_handler(Module) ->
368 4 case mongoose_lib:is_exported(Module, stop, 0) of
369 2 true -> Module:stop();
370 2 false -> ok
371 end.
372
373 -spec call_handler(handler_fun(), event_name(), labels(), config(), measurements()) -> any().
374 call_handler(HandlerFun, EventName, Labels, Config, Measurements) ->
375 59129 try
376 59129 HandlerFun(EventName, Labels, Config, Measurements)
377 catch
378 Class:Reason:StackTrace ->
379
:-(
?LOG_ERROR(#{what => event_handler_failed,
380 handler_fun => HandlerFun,
381 event_name => EventName, labels => Labels, config => Config,
382 measurements => Measurements,
383
:-(
class => Class, reason => Reason, stacktrace => StackTrace})
384 end.
Line Hits Source