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:

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