| 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 |  |     % Default collectors include VM metrics. | 
| 273 | 3 |     OldValue = application:get_env(prometheus, collectors) =:= undefined, | 
| 274 | 3 |     PID = whereis(prometheus_sup), | 
| 275 | 3 |     if is_pid(PID), NewValue =/= OldValue -> | 
| 276 | :-( |             modified; | 
| 277 |  |        true -> | 
| 278 | 3 |             ok | 
| 279 |  |     end. |