OTP logger backend that sends log events to Systemd's journald service. Pure Erlang implementation (no C dependencies).
Add to your rebar.config
{deps, [logger_journald]}.
to your sys.config
{kernel, [
{logger, [
{handler, my_handler, logger_journald_h, #{
level => info,
formatter => {logger_formtter, #{max_size => 4096}},
config => #{
socket_path => "/run/systemd/journal/socket", % optional
defaults => #{"MY_KEY" => "My value", % optional
"SYSLOG_IDENTIFIER" => my_release},
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.
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.
Handler keeps counting dropped messages and will periodically log how many messages were dropped
since last report (if any).
The way logger:log_event() is converted to a journald flat key-value structure is following:
msg
is formatted with formatter and sent asMESSAGE
fieldlevel
is converted to syslog's numerical value between 0 ("emergency") and 7 ("debug") and is sent asPRIORITY
fieldmeta
fields are encoded in a following wayfile
,line
andmfa
are encoded asCODE_FILE
,CODE_LINE
andCODE_FUNC
(inModule:Function/Arity
form)time
is encoded in RFC-3339 format and sent asSYSLOG_TIMESTAMP
fieldpid
is encoded as a string and sent asERL_PID
fieldgl
is encoded the same aspid
and sent asERL_GROUP_LEADER
domain
is 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.
Please, run the following before committing
make pre-commit
make test
make dialyzer