logger_journald
OTP logger backend that sends log events to Systemd's journald service. Pure Erlang implementation (no C dependencies).
Usage
Add to your rebar.config
{deps, [logger_journald]}.
to your sys.config
{kernel, [
{logger, [
{handler, my_handler, logger_journald_h, #{
level => info,
config => #{
socket_path => "/run/systemd/journal/socket", % optional
defaults => #{"MY_KEY" => "My value", % optional
"SYSLOG_IDENTIFIER" => my_release}
}
}}
]}
]}or do this somewhere in your code
logger:add_handler(my_handler, logger_journald_h,
#{config => #{defaults => #{"SYSLOG_IDENTIFIER" => my_release}}}).
I'd recommend to add at least one key to defaults to uniquely identify your application. The
recommended key name for that is SYSLOG_IDENTIFIER.
Other logger:handler_config() options (level, filters) should also work, but not
formatter, because format of journald packet is restricted.
Then start your app and send some logs. They can be seen, eg, like this (see man journalctl):
$ journalctl -f -o verbose _COMM=beam.smp SYSLOG_IDENTIFIER=my_release
Multiple instances of logger_journald_h handler can be started.
It's not currently possible to set logger_journald_h as default handler via sys.config,
because sys.config is applied at kernel application start time and logger_journald
application depends on kernel application (sort of cyclic dependency). However, disabling
default handler and installing just logger_journald_d works fine (but you might miss some
of early start-up related logs)
[{handler, default, undefined}, {handler, my_handler, logger_journald_d, #{}}].
There is also journald_log module available, which provides a thin wrapper with limited API for journald's control socket.
How it works?
When logger_journald application is started, it creates an empty supervisor.
When logger:add_handler/3 is called, logger_journald starts a new gen_server under
this supervisor, which holds open gen_udp UNIX-socket open to journald and some internal state.
When you call logger:log (or use ?LOG* macro), your log message is converted to be a flat
key-value structure in the same process where log is called and then sent to this gen_server,
which writes it to journld's socket via gen_udp.
Be careful! All the log messages targeting one handler are sent through the single gen_server! There is no backpressure support at the moment!
The way logger:log_event() is converted to a journald flat key-value structure is following:
msgis sent asMESSAGEfield-
If
msgis a{report, logger:report()}, then it is converted to a string withlogger:report_cb()function provided inmeta. -
If
msgis{io:format(), [any()]}, it is converted to a string withio_lib:format/2
-
If
levelis converted to syslog's numerical value between 0 ("emergency") and 7 ("debug") and is sent asPRIORITYfieldmetafields are encoded in a following wayfile,lineandmfaare encoded asCODE_FILE,CODE_LINEandCODE_FUNC(inModule:Function/Arityform)timeis encoded in RFC-3339 format and sent asSYSLOG_TIMESTAMPfieldpidis encoded as a string and sent asERL_PIDfieldglis encoded the same aspidand sent asERL_GROUP_LEADERdomainis encoded as dot-separated string (eg[otp, sasl]->otp.sasl) and sent asERL_DOMAIN-
all the custom metadata field names are converted to uppercase string and values are formatted
with
io_lib:format("~p", [Value])
Encoder works quite well with iolists / iodata values (they are sent as is), but it's slightly more optimized for a binary values.
Development
Please, run the following before committing
rebar3 as dev fmt
rebar3 as dev lint
rebar3 eunit
rebar3 edoc
rebar3 dialyzer