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},
formatter => {logger_formtter, #{max_size => 4096}},
sync_mode_qlen => 10,
drop_mode_qlen => 200
}
}}
]}
]}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.
formatter is limited: only logger_formatter callback module is supported and
legacy_header, template, time_* options are ignored (this may change in the future).
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 -t 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_sock 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.
Logger supports some basic overload protection, only sync_mode_qlen and drop_mode_qlen
are supported, they behave the same way as in standard logging handlers. See
User guide.
The way logger:log_event() is converted to a journald flat key-value structure is following:
msgis formatted with formatter and sent asMESSAGEfieldlevelis 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
make pre-commit
make test
make dialyzer