%% Copyright (C) 2025 by sysmocom - s.f.m.c. GmbH %% Author: Vadim Yanitskiy %% %% All Rights Reserved %% %% SPDX-License-Identifier: AGPL-3.0-or-later %% %% This program is free software; you can redistribute it and/or modify %% it under the terms of the GNU Affero General Public License as %% published by the Free Software Foundation; either version 3 of the %% License, or (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU Affero General Public License %% along with this program. If not, see . %% %% Additional Permission under GNU AGPL version 3 section 7: %% %% If you modify this Program, or any covered work, by linking or %% combining it with runtime libraries of Erlang/OTP as released by %% Ericsson on https://www.erlang.org (or a modified version of these %% libraries), containing parts covered by the terms of the Erlang Public %% License (https://www.erlang.org/EPLICENSE), the licensors of this %% Program grant you additional permission to convey the resulting work %% without the need to license the runtime libraries of Erlang/OTP under %% the GNU Affero General Public License. Corresponding Source for a %% non-source form of such a combination shall include the source code %% for the parts of the runtime libraries of Erlang/OTP used as well as %% that of the covered work. -module(logger_gsmtap). -behaviour(gen_server). %% public API -export([start/3, stop/1, log/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, terminate/2]). -define(GSMTAP_PORT, 4729). -define(GSMTAP_VERSION, 16#02). -define(GSMTAP_HDR_LEN, 16#04). %% in number of 32bit words -define(GSMTAP_TYPE_OSMOCORE_LOG, 16#10). %% ------------------------------------------------------------------ %% public API %% ------------------------------------------------------------------ -spec start(LAddr, RAddr, AppName) -> gen_server:start_ret() when LAddr :: inet:ip_address(), RAddr :: inet:ip_address(), AppName :: string(). start(LAddr, RAddr, AppName) -> gen_server:start(?MODULE, [LAddr, RAddr, AppName], []). -spec log(pid(), logger:log_event()) -> ok. log(Pid, LogEvent) -> gen_server:cast(Pid, {?FUNCTION_NAME, LogEvent}). -spec stop(pid()) -> ok. stop(Pid) -> gen_server:stop(Pid). %% ------------------------------------------------------------------ %% gen_server API %% ------------------------------------------------------------------ init([LAddr, RAddr, AppName]) -> {ok, Sock} = gen_udp:open(0, [binary, {ip, LAddr}, %% bind addr {reuseaddr, true}]), ok = gen_udp:connect(Sock, RAddr, ?GSMTAP_PORT), {ok, #{sock => Sock, app_name => AppName}}. handle_call(_Request, _From, S) -> {reply, {error, not_implemented}, S}. handle_cast({log, LogEvent}, #{sock := Sock, app_name := AppName} = S) -> PDU = gsmtap_pdu(LogEvent#{app_name => AppName}), gen_udp:send(Sock, PDU), {noreply, S}; handle_cast(_Request, S) -> {noreply, S}. terminate(_Reason, #{sock := Sock}) -> gen_udp:close(Sock), ok. %% ------------------------------------------------------------------ %% private API %% ------------------------------------------------------------------ -spec gsmtap_pdu(map()) -> binary(). gsmtap_pdu(#{msg := Msg, level := Level, app_name := AppName, meta := #{pid := Pid, time := Time} = Meta}) -> << ?GSMTAP_VERSION, ?GSMTAP_HDR_LEN, ?GSMTAP_TYPE_OSMOCORE_LOG, 16#00:(128 - 3 * 8), %% padding (Time div 1_000_000):32, %% seconds (Time rem 1_000_000):32, %% microseconds (charbuf(AppName, 16))/bytes, %% process name 16#00:32, %% dummy, Pid goes to subsys (log_level(Level)), 16#00:24, %% padding (charbuf(pid_to_list(Pid), 16))/bytes, %% subsys (charbuf(filename(Meta), 32))/bytes, %% filename (maps:get(line, Meta, 0)):32, %% line number (list_to_binary(msg2str(Msg)))/bytes %% the message >>. -type log_event_msg() :: {io:format(), [term()]} | {report, logger:report()} | {string, unicode:chardata()}. -spec msg2str(log_event_msg()) -> string(). msg2str({string, Str}) -> Str; msg2str({report, Report}) -> %% TODO: use report_cb() here io_lib:format("~p", [Report]); msg2str({FmtStr, Args}) -> io_lib:format(FmtStr, Args). filename(#{file := FileName}) -> filename:basename(FileName); filename(#{}) -> "(none)". -spec charbuf(Str0, Size) -> binary() when Str0 :: string(), Size :: non_neg_integer(). charbuf(Str0, Size) -> Str1 = string:slice(Str0, 0, Size - 1), %% truncate, if needed Str2 = string:pad(Str1, Size, trailing, 16#00), %% pad, if needed list_to_binary(Str2). -spec log_level(atom()) -> 0..255. log_level(debug) -> 1; log_level(info) -> 3; log_level(notice) -> 5; log_level(warning) -> 6; %% XXX: non-standard log_level(error) -> 7; log_level(critical) -> 8; log_level(alert) -> 9; %% XXX: non-standard log_level(emergency) -> 11; %% XXX: non-standard log_level(_) -> 255. %% XXX: non-standard %% vim:set ts=4 sw=4 et: