/home/runner/work/eturnal/eturnal/_build/test/cover/ct/mod_stats_prometheus.html

1 %%% eturnal STUN/TURN server module.
2 %%%
3 %%% Copyright (c) 2022-2023 Holger Weiss <holger@zedat.fu-berlin.de>.
4 %%% Copyright (c) 2022-2023 ProcessOne, SARL.
5 %%% All rights reserved.
6 %%%
7 %%% Licensed under the Apache License, Version 2.0 (the "License");
8 %%% you may not use this file except in compliance with the License.
9 %%% You may obtain a copy of the License at
10 %%%
11 %%% http://www.apache.org/licenses/LICENSE-2.0
12 %%%
13 %%% Unless required by applicable law or agreed to in writing, software
14 %%% distributed under the License is distributed on an "AS IS" BASIS,
15 %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 %%% See the License for the specific language governing permissions and
17 %%% limitations under the License.
18
19 %%% This module exports STUN/TURN metrics to Prometheus.
20
21 -module(mod_stats_prometheus).
22 -behaviour(eturnal_module).
23 -export([start/0,
24 stop/0,
25 handle_event/2,
26 options/0]).
27 -import(yval, [either/2, ip/0, port/0, bool/0, file/1]).
28
29 -include_lib("kernel/include/logger.hrl").
30 -define(PEM_FILE_NAME, "cert.pem").
31 -define(SIZE_BUCKETS,
32 [1024 * 4,
33 1024 * 32,
34 1024 * 256,
35 1024 * 1024,
36 1024 * 1024 * 4,
37 1024 * 1024 * 16,
38 1024 * 1024 * 64,
39 1024 * 1024 * 256,
40 1024 * 1024 * 1024]).
41 -define(TIME_BUCKETS,
42 [timer:minutes(1) div 1000,
43 timer:minutes(5) div 1000,
44 timer:minutes(15) div 1000,
45 timer:minutes(30) div 1000,
46 timer:hours(1) div 1000,
47 timer:hours(2) div 1000,
48 timer:hours(6) div 1000,
49 timer:hours(12) div 1000,
50 timer:hours(24) div 1000]).
51
52 %% API.
53
54 -spec start() -> {ok, eturnal_module:events()}.
55 start() ->
56 3 ?LOG_DEBUG("Starting ~s", [?MODULE]),
57 3 Opt = eturnal_module:get_opt(?MODULE, vm_metrics),
58 3 case {check_vm_metrics_opt(Opt), Opt} of
59 {ok, true} ->
60 3 ok;
61 {ok, false} ->
62 % See: prometheus.erl/src/prometheus_collector.erl
63
:-(
Collectors = [prometheus_boolean,
64 prometheus_counter,
65 prometheus_gauge,
66 prometheus_histogram,
67 prometheus_summary],
68
:-(
ok = application:set_env(
69 prometheus, collectors, Collectors, [{persistent, true}]);
70 {modified, _Opt} ->
71 % The 'prometheus' application doesn't support updating the list of
72 % collectors on configuration reload, and we cannot easily restart
73 % it, as the application controller might be blocking on our own
74 % startup.
75
:-(
?LOG_ERROR("New 'vm_metrics' setting requires restart")
76 end,
77 3 ok = eturnal_module:ensure_deps(?MODULE, get_deps()),
78 3 ok = declare_metrics(),
79 3 Addr = eturnal_module:get_opt(?MODULE, ip),
80 3 Port = eturnal_module:get_opt(?MODULE, port),
81 3 Root = get_document_root(), % Empty and unused.
82 3 case inets:start(httpd, [socket_opts(),
83 {bind_address, Addr},
84 {port, Port},
85 {server_root, Root},
86 {document_root, Root},
87 {server_name, "Prometheus Exporter"},
88 {modules, [prometheus_httpd]}]) of
89 {ok, _PID} ->
90 3 ok;
91 {error, {already_started, _PID}} ->
92
:-(
ok;
93 {error, Reason2} ->
94
:-(
exit(Reason2)
95 end,
96 3 {ok, [stun_query, turn_session_start, turn_session_stop, protocol_error]}.
97
98 -spec handle_event(eturnal_module:event(), eturnal_module:info()) -> ok.
99 handle_event(stun_query, Info) ->
100 5 on_stun_query(Info);
101 handle_event(turn_session_start, Info) ->
102 2 on_turn_session_start(Info);
103 handle_event(turn_session_stop, Info) ->
104 2 on_turn_session_stop(Info);
105 handle_event(protocol_error, Info) ->
106
:-(
on_protocol_error(Info).
107
108 -spec stop() -> ok.
109 stop() ->
110 3 ?LOG_DEBUG("Stopping ~s", [?MODULE]),
111 3 AddrPort = {eturnal_module:get_opt(?MODULE, ip),
112 eturnal_module:get_opt(?MODULE, port)},
113 3 ok = inets:stop(httpd, AddrPort).
114
115 -spec options() -> eturnal_module:options().
116 options() ->
117 3 {#{ip => either(any, ip()),
118 port => port(),
119 tls => bool(),
120 tls_crt_file => file(read),
121 tls_key_file => file(read),
122 vm_metrics => bool()},
123 [{defaults,
124 #{ip => any,
125 port => 8081,
126 tls => false,
127 tls_crt_file => none,
128 tls_key_file => none,
129 vm_metrics => true}}]}.
130
131 %% Internal functions.
132
133 -spec declare_metrics() -> ok.
134 declare_metrics() ->
135 3 SizeSpec = [{labels, [transport]}, {buckets, ?SIZE_BUCKETS}],
136 3 TimeSpec = [{labels, [transport]}, {buckets, ?TIME_BUCKETS}],
137 3 _ = prometheus_http_impl:setup(),
138 3 _ = prometheus_counter:declare(
139 [{name, eturnal_stun_requests_total},
140 {labels, [transport]},
141 {help, "STUN request count"}]),
142 3 _ = prometheus_counter:declare(
143 [{name, eturnal_turn_sessions_total},
144 {labels, [transport]},
145 {help, "TURN session count"}]),
146 3 _ = prometheus_gauge:declare(
147 [{name, eturnal_turn_open_sessions},
148 {help, "Number of currently active TURN sessions"} | SizeSpec]),
149 3 _ = prometheus_histogram:declare(
150 [{name, eturnal_turn_relay_sent_bytes},
151 {help, "Number of bytes sent to TURN peers"} | SizeSpec]),
152 3 _ = prometheus_histogram:declare(
153 [{name, eturnal_turn_relay_rcvd_bytes},
154 {help, "Number of bytes received from TURN peers"} | SizeSpec]),
155 3 _ = prometheus_histogram:declare(
156 [{name, eturnal_turn_session_duration_seconds},
157 {help, "Duration of TURN session in seconds"} | TimeSpec]),
158 3 _ = prometheus_counter:declare(
159 [{name, eturnal_protocol_error_total},
160 {labels, [transport, reason]},
161 {help, "STUN/TURN protocol error count"}]),
162 3 ok.
163
164 -spec on_stun_query(eturnal_module:info()) -> ok.
165 on_stun_query(#{transport := Transport}) ->
166 5 ?LOG_DEBUG("Observing STUN query for Prometheus"),
167 5 _ = prometheus_counter:inc(
168 eturnal_stun_requests_total, [Transport]),
169 5 ok.
170
171 -spec on_turn_session_start(eturnal_module:info()) -> ok.
172 on_turn_session_start(#{id := ID,
173 transport := Transport}) ->
174 2 ?LOG_DEBUG("Observing started TURN session ~s for Prometheus", [ID]),
175 2 _ = prometheus_gauge:inc(
176 eturnal_turn_open_sessions, [Transport]),
177 2 _ = prometheus_counter:inc(
178 eturnal_turn_sessions_total, [Transport]),
179 2 ok.
180
181 -spec on_turn_session_stop(eturnal_module:info()) -> ok.
182 on_turn_session_stop(#{id := ID,
183 transport := Transport,
184 sent_bytes := SentBytes,
185 rcvd_bytes := RcvdBytes,
186 duration := Duration}) ->
187 2 ?LOG_DEBUG("Observing stopped TURN session ~s for Prometheus", [ID]),
188 2 _ = prometheus_gauge:dec(
189 eturnal_turn_open_sessions, [Transport]),
190 2 _ = prometheus_histogram:observe(
191 eturnal_turn_session_duration_seconds, [Transport], Duration),
192 2 _ = prometheus_histogram:observe(
193 eturnal_turn_relay_rcvd_bytes, [Transport], RcvdBytes),
194 2 _ = prometheus_histogram:observe(
195 eturnal_turn_relay_sent_bytes, [Transport], SentBytes),
196 2 ok.
197
198 -spec on_protocol_error(eturnal_module:info()) -> ok.
199 on_protocol_error(#{transport := Transport,
200 reason := {_Code, Text}}) ->
201
:-(
?LOG_DEBUG("Observing protocol error for Prometheus: ~s", [Text]),
202
:-(
_ = prometheus_counter:inc(
203 eturnal_protocol_error_total, [Transport, Text]),
204
:-(
ok.
205
206 -spec socket_opts() -> {socket_type, ip_comm | {essl, proplists:proplist()}}.
207 socket_opts() ->
208 3 case eturnal_module:get_opt(?MODULE, tls) of
209 true ->
210
:-(
{CrtFile, KeyFile} = get_pem_files(),
211
:-(
{socket_type, {essl, [{certfile, CrtFile}, {keyfile, KeyFile}]}};
212 false ->
213 3 {socket_type, ip_comm}
214 end.
215
216 -spec get_deps() -> [eturnal_module:dep()].
217 get_deps() ->
218 3 case eturnal_module:get_opt(?MODULE, tls) of
219 true ->
220
:-(
[ssl, prometheus_httpd];
221 false ->
222 3 [prometheus_httpd]
223 end.
224
225 -spec get_pem_files() -> {file:filename(), file:filename()}.
226 get_pem_files() ->
227
:-(
case get_module_or_global_opt(tls_crt_file) of
228 CrtFile when is_binary(CrtFile) ->
229
:-(
case get_module_or_global_opt(tls_key_file) of
230 KeyFile when is_binary(KeyFile) ->
231
:-(
{CrtFile, KeyFile};
232 none ->
233
:-(
{CrtFile, CrtFile}
234 end;
235 none ->
236
:-(
?LOG_WARNING("TLS enabled for ~s without 'tls_crt_file', creating "
237
:-(
"self-signed certificate", [?MODULE]),
238
:-(
File = get_pem_file_path(),
239
:-(
ok = eturnal:create_self_signed(File),
240
:-(
{File, File}
241 end.
242
243 -spec get_pem_file_path() -> file:filename().
244 get_pem_file_path() ->
245
:-(
Path = filename:join(get_module_run_dir(), <<?PEM_FILE_NAME>>),
246
:-(
unicode:characters_to_list(Path).
247
248 -spec get_document_root() -> file:filename().
249 get_document_root() ->
250 3 DocDir = filename:join(get_module_run_dir(), <<"doc">>),
251 3 ok = filelib:ensure_dir(filename:join(DocDir, <<"file">>)),
252 3 unicode:characters_to_list(DocDir).
253
254 -spec get_module_run_dir() -> file:filename().
255 get_module_run_dir() ->
256 3 RunDir = filename:join(eturnal:get_opt(run_dir), ?MODULE),
257 3 ok = filelib:ensure_dir(filename:join(RunDir, <<"file">>)),
258 3 unicode:characters_to_list(RunDir).
259
260 -spec get_module_or_global_opt(eturnal_module:option()) -> term().
261 get_module_or_global_opt(Opt) ->
262
:-(
case eturnal_module:get_opt(?MODULE, Opt) of
263 Value when Value =:= undefined;
264 Value =:= none ->
265
:-(
eturnal:get_opt(Opt);
266 Value ->
267
:-(
Value
268 end.
269
270 -spec check_vm_metrics_opt(boolean()) -> ok | modified.
271 check_vm_metrics_opt(NewValue) ->
272 3 PID = whereis(prometheus_sup),
273 3 OldValue = application:get_env(prometheus, collectors),
274 3 if is_pid(PID),
275 ((NewValue =:= true) and (OldValue =/= undefined)) or
276 (NewValue =/= true) and (OldValue =:= undefined) ->
277
:-(
modified;
278 true ->
279 3 ok
280 end.
Line Hits Source