./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 %% gen_server callbacks
14 -export([init/1, handle_call/3, handle_cast/2, code_change/3, handle_info/2, terminate/2]).
15
16 -ignore_xref([start_link/0, set_up/3, tear_down/2, span/4]).
17
18 -include("mongoose.hrl").
19 -include("mongoose_config_spec.hrl").
20
21 -type event_name() :: atom().
22 -type labels() :: #{host_type => mongooseim:host_type()}. % to be extended
23 -type metrics() :: #{atom() => spiral | histogram}. % to be extended
24 -type measurements() :: #{atom() => term()}.
25 -type spec() :: {event_name(), labels(), config()}.
26 -type config() :: #{metrics => metrics()}. % to be extended
27 -type handler_key() :: atom(). % key in the `instrumentation' section of the config file
28 -type handler_fun() :: fun((event_name(), labels(), config(), measurements()) -> any()).
29 -type handlers() :: {[handler_fun()], config()}.
30 -type execution_time() :: integer().
31 -type measure_fun(Result) :: fun((execution_time(), Result) -> measurements()).
32
33 -callback config_spec() -> mongoose_config_spec:config_section().
34 -callback set_up(event_name(), labels(), config()) -> boolean().
35 -callback handle_event(event_name(), labels(), config(), measurements()) -> any().
36
37 -optional_callbacks([config_spec/0]).
38
39 -export_type([event_name/0, labels/0, config/0, measurements/0, spec/0, handlers/0]).
40
41 %% API
42
43 %% @doc Specifies the `instrumentation' section of the config file
44 -spec config_spec() -> mongoose_config_spec:config_section().
45 config_spec() ->
46 53 Items = [{atom_to_binary(Key), config_spec(Key)} || Key <- all_handler_keys()],
47 53 #section{items = maps:from_list(Items),
48 wrap = global_config,
49 include = always}.
50
51 -spec start_link() -> gen_server:start_ret().
52 start_link() ->
53 53 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
54
55 %% @doc Saves the state to a persistent term, improving performance of `execute' and `span'.
56 %% On the other hand, future calls to `set_up' or `tear_down' will update the persistent term,
57 %% which makes them less performant.
58 %% You should call this function only once - after the initial setup, but before handling any load.
59 -spec persist() -> ok.
60 persist() ->
61 53 gen_server:call(?MODULE, persist).
62
63 %% @doc Sets up instrumentation for multiple events.
64 %% @see set_up/3
65 -spec set_up([spec()]) -> ok.
66 set_up(Specs) ->
67 270 lists:foreach(fun({EventName, Labels, Config}) -> set_up(EventName, Labels, Config) end, Specs).
68
69 %% @doc Tears down instrumentation for multiple events.
70 %% @see tear_down/2
71 -spec tear_down([spec()]) -> ok.
72 tear_down(Specs) ->
73 270 lists:foreach(fun({EventName, Labels, _Config}) -> tear_down(EventName, Labels) end, Specs).
74
75 %% @doc Sets up instrumentation for an event identified by `EventName' and `Labels'
76 %% according to `Config'. Fails if the event is already registered, or if the keys of `Labels'
77 %% are different than for already registered events with `EventName'.
78 -spec set_up(event_name(), labels(), config()) -> ok.
79 set_up(EventName, Labels, Config) ->
80 1518 case gen_server:call(?MODULE, {set_up, EventName, Labels, Config}) of
81 1518 ok -> ok;
82
:-(
{error, ErrorMap} -> error(ErrorMap)
83 end.
84
85 %% @doc Tears down instrumentation for an event identified by `EventName' and `Labels'.
86 %% This operation is idempotent.
87 -spec tear_down(event_name(), labels()) -> ok.
88 tear_down(EventName, Labels) ->
89 1518 gen_server:call(?MODULE, {tear_down, EventName, Labels}).
90
91 %% @doc Calls `F', measuring its result with `MeasureF', and calls attached event handlers.
92 %% @see span/5
93 -spec span(event_name(), labels(), fun(() -> Result), measure_fun(Result)) -> Result.
94 span(EventName, Labels, F, MeasureF) ->
95
:-(
span(EventName, Labels, F, [], MeasureF).
96
97 %% @doc Calls `F' with `Args', measuring its execution time.
98 %% The time and the result are passed to `MeasureF', which returns measurements.
99 %% The measurements are then passed to all handlers attached to
100 %% the event identified by `EventName' and `Labels'.
101 %% Fails without calling `F' if the event is not registered.
102 -spec span(event_name(), labels(), fun((...) -> Result), list(), measure_fun(Result)) -> Result.
103 span(EventName, Labels, F, Args, MeasureF) ->
104 13296 Handlers = get_handlers(EventName, Labels),
105 13296 {Time, Result} = timer:tc(F, Args),
106 13296 handle_event(EventName, Labels, MeasureF(Time, Result), Handlers),
107 13296 Result.
108
109 %% @doc Executes all handlers attached to the event identified by `EventName' and `Labels',
110 %% passing `Measurements' to them. Fails if the event is not registered.
111 -spec execute(event_name(), labels(), measurements()) -> ok.
112 execute(EventName, Labels, Measurements) ->
113 1630 Handlers = get_handlers(EventName, Labels),
114 1629 handle_event(EventName, Labels, Measurements, Handlers).
115
116 %% gen_server callbacks
117
118 -type state() :: #{event_name() => #{labels() => handlers()}}.
119
120 -spec init([]) -> {ok, state()}.
121 init([]) ->
122 53 erlang:process_flag(trap_exit, true), % Make sure that terminate is called
123 53 persistent_term:erase(?MODULE), % Prevent inconsistency when restarted after a kill
124 53 {ok, #{}}.
125
126 -spec handle_call(any(), gen_server:from(), state()) ->
127 {reply, ok | {ok, handlers()} | {error, map()}, state()}.
128 handle_call({set_up, EventName, Labels, Config}, _From, State) ->
129 1518 case set_up_and_register(EventName, Labels, Config, State) of
130 {error, _} = Error ->
131
:-(
{reply, Error, State};
132 NewState = #{} ->
133 1518 update_if_persisted(State, NewState),
134 1518 {reply, ok, NewState}
135 end;
136 handle_call({tear_down, EventName, Labels}, _From, State) ->
137 1518 NewState = deregister(EventName, Labels, State),
138 1518 update_if_persisted(State, NewState),
139 1518 {reply, ok, NewState};
140 handle_call(persist, _From, State) ->
141 53 persistent_term:put(?MODULE, State),
142 53 {reply, ok, State};
143 handle_call({lookup, EventName, Labels}, _From, State) ->
144
:-(
{reply, lookup(EventName, Labels, State), State};
145 handle_call(Request, From, State) ->
146
:-(
?UNEXPECTED_CALL(Request, From),
147
:-(
{reply, {error, #{what => unexpected_call, request => Request}}, State}.
148
149 -spec handle_cast(any(), state()) -> {noreply, state()}.
150 handle_cast(Msg, State) ->
151
:-(
?UNEXPECTED_CAST(Msg),
152
:-(
{noreply, State}.
153
154 -spec handle_info(any(), state()) -> {noreply, state()}.
155 handle_info(Info, State) ->
156
:-(
?UNEXPECTED_INFO(Info),
157
:-(
{noreply, State}.
158
159 -spec terminate(any(), state()) -> ok.
160 terminate(_Reason, _State) ->
161 54 persistent_term:erase(?MODULE),
162 54 ok.
163
164 -spec code_change(any(), state(), any()) -> {ok, state()}.
165 code_change(_OldVsn, State, _Extra) ->
166
:-(
{ok, State}.
167
168 %% Internal functions
169
170 -spec update_if_persisted(state(), state()) -> ok.
171 update_if_persisted(State, NewState) ->
172 3036 try persistent_term:get(?MODULE) of
173 3036 State -> persistent_term:put(?MODULE, NewState)
174 catch
175
:-(
error:badarg -> ok
176 end.
177
178 -spec set_up_and_register(event_name(), labels(), config(), state()) -> state() | {error, map()}.
179 set_up_and_register(EventName, Labels, Config, State) ->
180 1518 LabelKeys = label_keys(Labels),
181 1518 case State of
182 #{EventName := #{Labels := _}} ->
183
:-(
{error, #{what => event_already_registered,
184 event_name => EventName, labels => Labels}};
185 #{EventName := HandlerMap} ->
186 56 {ExistingLabels, _, _} = maps:next(maps:iterator(HandlerMap)),
187 56 case label_keys(ExistingLabels) of
188 LabelKeys ->
189 56 Handlers = do_set_up(EventName, Labels, Config),
190 56 State#{EventName := HandlerMap#{Labels => Handlers}};
191 ExistingKeys ->
192
:-(
{error, #{what => inconsistent_labels,
193 event_name => EventName, labels => Labels,
194 existing_label_keys => ExistingKeys}}
195 end;
196 #{} ->
197 1462 Handlers = do_set_up(EventName, Labels, Config),
198 1462 State#{EventName => #{Labels => Handlers}}
199 end.
200
201 -spec do_set_up(event_name(), labels(), config()) -> handlers().
202 do_set_up(EventName, Labels, Config) ->
203 1518 AllModules = handler_modules(),
204 1518 UsedModules = lists:filter(fun(Mod) -> Mod:set_up(EventName, Labels, Config) end, AllModules),
205 1518 {[fun Mod:handle_event/4 || Mod <- UsedModules], Config}.
206
207 -spec deregister(event_name(), labels(), state()) -> state().
208 deregister(EventName, Labels, State) ->
209 1518 case State of
210 #{EventName := HandlerMap} ->
211 1518 case maps:remove(Labels, HandlerMap) of
212 Empty when Empty =:= #{} ->
213 1462 maps:remove(EventName, State);
214 NewHandlerMap ->
215 56 State#{EventName := NewHandlerMap}
216 end;
217 #{} ->
218
:-(
State
219 end.
220
221 -spec lookup(event_name(), labels()) -> {ok, handlers()} | {error, map()}.
222 lookup(EventName, Labels) ->
223 14926 try persistent_term:get(?MODULE) of
224 State ->
225 14926 lookup(EventName, Labels, State)
226 catch
227 %% Although persist/0 should be called before handling traffic,
228 %% some instrumented events might happen before that, and they shouldn't fail.
229 error:badarg ->
230
:-(
?LOG_INFO(#{what => mongoose_instrument_lookup_without_persistent_term,
231
:-(
event_name => EventName, labels => Labels}),
232
:-(
gen_server:call(?MODULE, {lookup, EventName, Labels})
233 end.
234
235 -spec lookup(event_name(), labels(), state()) -> {ok, handlers()} | {error, map()}.
236 lookup(EventName, Labels, State) ->
237 14926 case State of
238 #{EventName := #{Labels := Handlers}} ->
239 14925 {ok, Handlers};
240 #{} ->
241 1 {error, #{what => event_not_registered, event_name => EventName, labels => Labels}}
242 end.
243
244 -spec label_keys(labels()) -> [atom()].
245 label_keys(Labels) ->
246 1574 lists:sort(maps:keys(Labels)).
247
248 -spec get_handlers(event_name(), labels()) -> handlers().
249 get_handlers(EventName, Labels) ->
250 14926 case lookup(EventName, Labels) of
251 14925 {ok, Handlers} -> Handlers;
252 1 {error, ErrorMap} -> error(ErrorMap)
253 end.
254
255 -spec handle_event(event_name(), labels(), measurements(), handlers()) -> ok.
256 handle_event(Event, Labels, Measurements, {EventHandlers, Config}) ->
257 14925 lists:foreach(fun(Handler) -> Handler(Event, Labels, Config, Measurements) end, EventHandlers).
258
259 -spec handler_modules() -> [module()].
260 handler_modules() ->
261 1518 [handler_module(Key) || Key <- maps:keys(mongoose_config:get_opt(instrumentation))].
262
263 -spec handler_module(handler_key()) -> module().
264 handler_module(Key) ->
265 4713 list_to_existing_atom("mongoose_instrument_" ++ atom_to_list(Key)).
266
267 -spec config_spec(handler_key()) -> mongoose_config_spec:config_section().
268 config_spec(Key) ->
269 159 Module = handler_module(Key),
270 159 case mongoose_lib:is_exported(Module, config_spec, 0) of
271 53 true -> Module:config_spec();
272 106 false -> #section{}
273 end.
274
275 -spec all_handler_keys() -> [handler_key()].
276 all_handler_keys() ->
277 53 [prometheus, exometer, log].
Line Hits Source