%% Licensed to the Apache Software Foundation (ASF) under one
%% or more contributor license agreements. See the NOTICE file
%% distributed with this work for additional information
%% regarding copyright ownership. The ASF licenses this file
%% to you under the Apache License, Version 2.0 (the
%% "License"); you may not use this file except in compliance
%% with the License. You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% @doc
%% Logger handler for sending messages to systemd's `journal'.
%%
%% == Usage ==
%%
%% Run this after the `systemd' application is started:
%%
%% ```
%% logger:add_handler(journal, systemd_journal_h, #{}).
%% '''
%%
%% == Options ==
%%
%%
%% - `fields :: [field_definition()]'
%% - Contains list of all fields that will be passed to the `journald'.
%%
%% Defaults to:
%%
%% ```
%% [syslog_timestamp,
%% syslog_pid,
%% priority,
%% {"ERL_PID", pid},
%% {"CODE_FILE", file},
%% {"CODE_LINE", line},
%% {"CODE_MFA", mfa}]
%% '''
%%
%% See {@section Fields} below.
%%
%% - `report_cb :: fun ((logger:report()) -> [field()]'
%% - Function that takes `Prefix' and Logger's report and returns list
%% of 2-ary tuples where first one MUST contain only uppercase ASCII
%% letters, digits and underscore characters, and must not start with
%% underscore. Field name and second one is field value in form of
%% `iolist()'. It is important to note that value can contain any data,
%% and does not need to be in any encoding; it can even be binary.
%%
%% === Example ===
%%
%% ```
%% my_formatter(Prefix, #{field := Field}) when is_integer(Field) ->
%% [
%% {[Prefix,"_FIELD"], io_lib:format("~.16B", [Field]}
%% ].
%% '''
%%
%% Remember that all field names MUST NOT start with the underscore,
%% otherwise `journald' can ignore them. Such behaviour is not enforced on
%% data returned by `report_cb' and it is left up to the implementor to
%% remember it.
%%
%%
%% == Fields ==
%%
%% Fields list contain definition of fields that will be presented in the log
%% message fed into `journald'. Few of them have special meaning and you can
%% see list of them in the
%% `systemd.journal-fields(7)' manpage.
%%
%% Metakeys (i.e. atoms) in `fields' list will be sent to
%% the `journald' as a uppercased atom names.
%%
%% Entries in form of `{Name :: field_name(), metakey()}' will use `Name'
%% as the field name. `Name' will be checked if it is correct `journald' field
%% name (i.e. contains only ASCII letters, digits, and underscores,
%% additionally do not start with underscore).
%%
%% Entries in form of `{Name :: field_name(), Data :: iolist()}' will use
%% `Name' as field name and will contain `Data' as a literal.
%%
%% If entry data is empty or not set then it will be ommited in the output.
%%
%% === Special fields ===
%%
%% Special fields availables:
%%
%%
%% - `level'
%% - Log level presented as string.
%% - `priority'
%% - Log level presented as decimal representation of syslog level.
%% - `os_pid'
%% - OS PID for current Erlang process. This is NOT Erlang PID.
%%
%% - `mfa'
%% - Calling function presented in form `Module:Function/Arity'.
%% - `time'
%% - Timestamp of log message presented in RFC3339 format in UTC.
%%
%%
%% Otherwise field is treated as a entry key where `key' is equivalent of
%% `[key]' and is used as a list of atoms to extract data from the metadata map.
%%
%% === Syslog compatibility ===
%%
%% To provide better compatibility and user convinience:
%%
%%
%% - `syslog_pid'
%% - Will work exactly the same as `{"SYSLOG_PID", os_pid}'.
%% - `syslog_timestamp'
%% - Will work exactly the same as `{"SYSLOG_TIMESTAMP", time}'.
%%
%%
%% @since 0.3.0
%% @end
-module(systemd_journal_h).
-behaviour(enough).
-include("systemd_internal.hrl").
-define(JOURNAL_SOCKET, <<"/run/systemd/journal/socket">>).
% logger handler callbacks
-export([adding_handler/1,
changing_config/3,
filter_config/1,
removing_handler/1,
log/2]).
% gen_server callbacks
-export([start_link/2,
init/1,
handle_load/2,
handle_call/3,
handle_cast/2]).
-define(FORMATTER, {logger_formatter, #{}}).
-define(CHILD_SPEC(Id, Args), #{id => Id,
start => {?MODULE, start_link, Args},
restart => temporary}).
-define(DEFAULT_FIELDS, [{"SYSLOG_TIMESTAMP", time},
{"SYSLOG_PID", os_pid},
{"PRIORITY", priority},
{"ERL_PID", pid},
{"CODE_FILE", file},
{"CODE_LINE", line},
{"CODE_MFA", mfa}]).
% -----------------------------------------------------------------------------
% Logger Handler
%% @hidden
-spec adding_handler(logger:handler_config()) -> {ok, logger:handler_config()} |
{error, term()}.
adding_handler(HConfig) ->
Config0 = maps:get(config, HConfig, #{}),
case get_path(Config0) of
false ->
{error, no_journal_socket};
{Path, Config} ->
do_add_handler(Path, Config, HConfig)
end.
do_add_handler(Path, Config, #{id := Id} = HConfig) ->
case validate_config(Config) of
ok ->
Fields = [translate_field(Field)
|| Field <- maps:get(fields, Config, ?DEFAULT_FIELDS)],
case start_connection(Id, Config) of
{ok, Pid, OlpRef} ->
{ok, HConfig#{config => Config#{pid => Pid,
fields => Fields,
olp_ref => OlpRef,
path => Path}}};
Err ->
Err
end;
Error -> Error
end.
-ifdef(TEST).
get_path(Config) ->
case maps:is_key(path, Config) of
true -> maps:take(path, Config);
false -> {{local, ?JOURNAL_SOCKET}, Config}
end.
-else.
get_path(Config) ->
case file:read_file_info(?JOURNAL_SOCKET) of
{error, enoent} -> false;
{ok, _} -> {{local, ?JOURNAL_SOCKET}, Config}
end.
-endif.
%% @hidden
changing_config(update, #{config := OldHConfig}, NewConfig) ->
NewHConfig = maps:get(config, NewConfig, #{}),
case validate_config(NewHConfig) of
ok ->
Fields = case maps:is_key(fields, NewHConfig) of
true ->
NewFields = maps:get(fields, NewHConfig),
[translate_field(Field) || Field <- NewFields];
false ->
maps:get(fields, OldHConfig)
end,
{ok, NewConfig#{config => OldHConfig#{fields := Fields}}};
Error -> Error
end;
changing_config(set, #{config := OldHConfig}, NewConfig) ->
NewHConfig = maps:get(config, NewConfig, #{}),
case validate_config(NewHConfig) of
ok ->
Fields = maps:get(fields, NewHConfig, ?DEFAULT_FIELDS),
Formatted = [translate_field(Field) || Field <- Fields],
{ok, NewConfig#{config => OldHConfig#{fields := Formatted}}};
Error -> Error
end.
translate_field(syslog_timestamp) -> {"SYSLOG_TIMESTAMP", time};
translate_field(syslog_pid) -> {"SYSLOG_PID", os_pid};
translate_field(Atom) when is_atom(Atom) -> {Atom, Atom};
translate_field({_Name, _Data} = Field) -> Field.
validate_config(Config0) when is_map(Config0) ->
Config = maps:without([pid, olp_ref, path], Config0),
do_validate(maps:to_list(Config)).
do_validate([{fields, Fields} | Rest]) ->
case check_fields(Fields) of
ok -> do_validate(Rest);
Error -> Error
end;
do_validate([{sync_mode_qlen, _} | Rest]) -> do_validate(Rest);
do_validate([{drop_mode_qlen, _} | Rest]) -> do_validate(Rest);
do_validate([{flush_qlen, _} | Rest]) -> do_validate(Rest);
do_validate([{burst_limit_enable, _} | Rest]) -> do_validate(Rest);
do_validate([{burst_limit_max_count, _} | Rest]) -> do_validate(Rest);
do_validate([{burst_limit_window_time, _} | Rest]) -> do_validate(Rest);
do_validate([{overload_kill_enable, _} | Rest]) -> do_validate(Rest);
do_validate([{overload_kill_qlen, _} | Rest]) -> do_validate(Rest);
do_validate([{overload_kill_mem_size, _} | Rest]) -> do_validate(Rest);
do_validate([{overload_kill_restart_after, _} | Rest]) -> do_validate(Rest);
do_validate([]) -> ok;
do_validate([Option | _]) ->
{error, {invalid_option, Option}}.
-define(IS_STRING(Name), (is_binary(Name) orelse is_list(Name))).
check_fields([Atom | Rest])
when is_atom(Atom) ->
Name = atom_to_list(Atom),
case check_name(Name) of
true -> check_fields(Rest);
false -> {error, {name_invalid, Name}}
end;
check_fields([{Atom, _} | Rest])
when is_atom(Atom) ->
check_fields([Atom | Rest]);
check_fields([{Name, _} | Rest])
when ?IS_STRING(Name) ->
case check_name(unicode:characters_to_list(Name)) of
true -> check_fields(Rest);
false -> {error, {name_invalid, Name}}
end;
check_fields([]) ->
ok;
check_fields([Unknown | _]) ->
{error, {invalid_field, Unknown}}.
check_name([C|Rest])
when $A =< C, C =< $Z;
$a =< C, C =< $z;
$0 =< C, C =< $9 ->
check_name_rest(Rest);
check_name(_) ->
false.
check_name_rest([C|Rest])
when $A =< C, C =< $Z;
$a =< C, C =< $z;
$0 =< C, C =< $9;
C == $_ ->
check_name_rest(Rest);
check_name_rest([]) ->
true;
check_name_rest(_) ->
false.
%% @hidden
-spec filter_config(logger:handler_config()) -> logger:handler_config().
filter_config(#{config := Config0} = HConfig) ->
Config = maps:without([pid, olp_ref, path], Config0),
HConfig#{config => Config}.
start_connection(Id, Config) ->
case supervisor:start_child(?SUPERVISOR, ?CHILD_SPEC(Id, [Id, Config])) of
{ok, Pid, OlpRef} -> {ok, Pid, OlpRef};
{error, Error} -> {error, {spawn_error, Error}}
end.
%% @hidden
-spec removing_handler(logger:handler_config()) -> ok.
removing_handler(#{config := #{pid := Pid}}) ->
ok = enough:stop(Pid),
ok.
%% @hidden
-spec log(logger:log_event(), logger:handler_config()) -> ok.
log(LogEvent, #{config := Config} = HConfig) ->
#{olp_ref := OlpRef, path := Path, fields := Fields} = Config,
{FMod, FConf} = maps:get(formatter, HConfig, ?FORMATTER),
Msg0 = FMod:format(LogEvent, FConf),
case string:is_empty(Msg0) of
false ->
FieldsData = [{Name, get_field(Field, LogEvent)}
|| {Name, Field} <- Fields],
Msg = unicode:characters_to_binary(Msg0),
Data = systemd_protocol:encode([{"MESSAGE", Msg} | FieldsData]),
ok = enough:load(OlpRef, {send, Data, Path});
true -> ok
end.
get_field(os_pid, _LogEvent) ->
os:getpid();
get_field(time, #{meta := #{time := Time}}) ->
calendar:system_time_to_rfc3339(Time, [{unit, microsecond},
{offset, "Z"}]);
get_field(mfa, #{meta := #{mfa := {M, F, A}}}) ->
io_lib:format("~tp:~tp/~B", [M, F, A]);
get_field(priority, #{level := Level}) ->
level_to_char(Level);
get_field(level, #{level := Level}) ->
atom_to_binary(Level, utf8);
get_field(Metakey, #{meta := Meta})
when is_atom(Metakey) orelse
(is_list(Metakey) andalso is_atom(hd(Metakey))) ->
case get_meta(Metakey, Meta) of
undefined -> "";
Data -> to_string(Data)
end;
get_field(Iolist, _) when is_list(Iolist) orelse is_binary(Iolist) ->
Iolist.
get_meta([], Data) ->
Data;
get_meta([Atom | Rest], Meta) when is_map(Meta) ->
case maps:get(Atom, Meta, undefined) of
undefined -> undefined;
Next -> get_meta(Rest, Next)
end;
get_meta(Atom, Meta) when is_atom(Atom) ->
maps:get(Atom, Meta, undefined);
get_meta(_, _) ->
undefined.
to_string(Atom) when is_atom(Atom) ->
atom_to_list(Atom);
to_string(Pid) when is_pid(Pid) ->
pid_to_list(Pid);
to_string(Ref) when is_reference(Ref) ->
ref_to_list(Ref);
to_string(Int) when is_integer(Int) ->
integer_to_list(Int);
to_string(List) when is_list(List) ->
case printable_list(List) of
true -> List;
false -> io_lib:format("~tp", [List])
end;
to_string(Bin) when is_binary(Bin) ->
case printable_list(binary_to_list(Bin)) of
true -> Bin;
false -> io_lib:format("~tp", [Bin])
end;
to_string(X) ->
io_lib:format("~tp", [X]).
printable_list([]) ->
false;
printable_list(X) ->
io_lib:printable_list(X).
level_to_char(debug) -> "7";
level_to_char(info) -> "6";
level_to_char(notice) -> "5";
level_to_char(warning) -> "4";
level_to_char(error) -> "3";
level_to_char(critical) -> "2";
level_to_char(alert) -> "1";
level_to_char(emergency) -> "0".
% -----------------------------------------------------------------------------
% Socket handler
%% @hidden
start_link(Id, Opts0) ->
Opts = maps:with([
sync_mode_qlen,
drop_mode_qlen,
flush_qlen,
burst_limit_enable,
burst_limit_max_count,
burst_limit_window_time,
overload_kill_enable,
overload_kill_qlen,
overload_kill_mem_size,
overload_kill_restart_after], Opts0),
enough:start_link(Id, ?MODULE, [], Opts).
%% @hidden
init(_Arg) ->
% We never receive on this socket, so we set {active, false}
{ok, Socket} = gen_udp:open(0, [binary, local, {active, false}]),
{ok, Socket}.
%% @hidden
handle_load({send, Message, Path}, Socket) ->
gen_udp:send(Socket, Path, Message),
Socket.
%% @hidden
handle_call(stop, _Ref, Socket) ->
{stop, normal, ok, Socket}.
%% @hidden
handle_cast(_Msg, Socket) ->
{noreply, Socket}.