Elixir has a Logger module used for, well, logging. By default however there is no integration with syslog. Thankfully for us, the syslog protocol is both simple and lenient. We’ll build a small implementation to seamlessly send our logging messages to syslog.

The RFC for syslog is available here: https://tools.ietf.org/html/rfc5424#section-6.1

Syslog preliminaries

The first thing to check is whether you have syslog listening for incoming messages.

vagrant@vagrant:~$ netstat -a -u
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
udp        0      0 *:bootpc                *:*

syslog listens on UDP port 514, and clearly it’s not open. If you’re using Debian/Ubuntu you’ll need to update /etc/rsyslog.conf and uncomment the last 2 lines of the section below:

# provides UDP syslog reception
#module(load="imudp")
#input(type="imudp" port="514")

Followed by a service rsyslog restart. Running the same command above, we see we’re now listening in!

vagrant@vagrant:~$ netstat -a -u
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
udp        0      0 *:syslog                *:*
udp        0      0 *:bootpc                *:*
udp6       0      0 [::]:syslog             [::]:*

Now that we have this out of the way, how do we exactly publish a message? That’s where nc comes in. As per the protocol defined here a message must start with <facility*8+severity>, where facility and severity are both numbers from 0 to 23 and 0 to 7 respectively (the meanings of which are defined in the RFC).

vagrant@vagrant:/shared/crashburn$ echo '<14>the quick brown fox' | nc -v -w1 -u localhost 514
Connection to localhost 514 port [udp/syslog] succeeded!
vagrant@vagrant:/shared/crashburn$ tail -1 /var/log/syslog
Feb 11 06:47:45 the quick brown fox

Note that if you’re generating a lot of messages, you’re probably better off using tail -f. So in theory, sending messages to syslog shouldn’t be much more complicated than opening a UDP connection to 514 and sending in a message (famous last words!).

Creating a UDP connection to syslog

To connect to the syslog server we’ll need to create a UDP connection. For this we leverage Erlang’s gen_udp module:

iex(10)> {ok, Socket} = :gen_udp.open(514)
** (MatchError) no match of right hand side value: {:error, :eacces}

Woops - it turns out that 514 is a privileged port! As such we can either start iex as root or create a pipe from a non-privilege port to 514. Either option works but the latter meant I didn’t have to restart the shell. To kick this off, run socat udp4-listen:5514,reuseaddr,fork udp4-sendto:127.0.0.1:514 which will redirect port 5514 to 514 on localhost. With this in place, let’s try again:

iex(8)> {:ok, socket} = :gen_udp.open(5514, [:binary, reuseaddr: true])
{:ok, #Port<0.4894>}
iex(9)> :gen_udp.send(socket, {127,0,0,1}, 514, "<14>pluto elixir is sending a message")
:ok

And looking at syslog:

vagrant@vagrant:~$ tail -1 /var/log/syslog
Feb 11 18:15:59 pluto elixir is sending a message

We now need to integrate the above with the Logger module.

Wiring this up with the logger

This isn’t a tutorial on the Logger module but in a nutshell it can be used as such:

iex(10)> require Logger
Logger
iex(11)> Logger.info("this is a message at the info level")

18:20:36.895 [info]  this is a message at the info level
:ok

The module is documented here and by default uses the :console backend. We’ll use the source code for :console as a template. We start by creating init and terminate methods:

defmodule SyslogLogger do

  @behaviour :gen_event

  def init(o) do
    IO.puts("init received #{inspect o}")
    {:ok, {}}
  end

  def terminate(_reason, _state) do
    :ok
  end

end

Which, as expected, doesn’t do much at all:

iex(26)> Logger.add_backend(SyslogLogger)
init received SyslogLogger
{:ok, #PID<0.158.0>}
iex(27)> Logger.remove_backend(SyslogLogger)
:ok

Custom backends implement gen_event. We change that to take a port number as a configuration option, and create a UDP connection:

defmodule SyslogLogger do

  @behaviour :gen_event

  def init({__MODULE__, portnumber}) do
    IO.puts("init received #{inspect portnumber}")
    state = %{socket: nil, port: nil}
    {:ok, init(portnumber, state)}
  end

  defp init(portnumber, state) do
    {:ok, socket} = :gen_udp.open(portnumber, [:binary, reuseaddr: true])
    %{state | port: portnumber, socket: socket}
  end

  def handle_event(_event, state) do
    {:ok, state}
  end

  def terminate(_reason, %{socket: socket} = state) do
    IO.puts("closing the socket: #{inspect socket}")
    :gen_udp.close(socket)
  end

end

Again, not much to see - the meat of the logger is in the hande_event call, which we define as follows:

  def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do
    IO.puts("sending #{msg} to syslog")
    ret = :gen_udp.send(state.socket, {127,0,0,1}, state.port, "<14>#{msg}")
    {:ok, state}
  end

This does nothing more than send msg across to syslog, with a pre-canned loglevel. We confirm this works as expected:

ex(89)> Logger.add_backend({SyslogLogger, 5514})
init received 5514
connected to 5514 with socket #Port<0.4836>
{:ok, #PID<0.237.0>}
iex(90)> Logger.info("foobar")

21:05:17.205 [info]  foobar
sending foobar to syslog
:ok
iex(91)> Logger.remove_backend({SyslogLogger, 5514})
closing the socket: #Port<0.4836>
:ok

And looking at syslog:

vagrant@vagrant:/shared/crashburn$ tail -1 /var/log/syslog                                               │
Feb 12 21:05:17 foobar   

Conclusion

This is a very crude logger that does not even respect the log level - it simply forwards messages to syslog with a canned severity level - and making the necessary changes to take this into account (as well as logging to a remote syslog server) is left as an exercise to the reader (!). Regardless it should hopefully highlight the simplicity with which one can plug in their own backends into the Logger module.

References

  • https://github.com/elixir-lang/elixir/blob/master/lib/logger/lib/logger/backends/console.ex
  • http://elixir-lang.org/getting-started/mix-otp/task-and-gen-tcp.html
  • http://andrealeopardi.com/posts/handling-tcp-connections-in-elixir/
  • https://github.com/onkel-dirtus/logger_file_backend
  • http://reganmian.net/blog/2015/08/26/email-notifications-about-errors-in-elixir/