Format your log messages in Python

Getting log messages into the desired format can sometimes be a problem, but with syslog-ng you can use Python to get exactly the format you need. The syslog-ng Python template function allows you to write custom templates for syslog-ng in Python. It was available in syslog-ng already for almost two years, but somehow stayed under the radar for me. The Python template function can work on the whole log message (which is passed on to it automatically as an object) or on the data received as argument.

In this blog I show you a simple use of the Python parser: resolving IP addresses to host names. I also show you a nice new feature, the logger method, which enables you to log to syslog-ng’s internal() log source instead of writing logs from Python to stdout. This way you can follow what your Python code does even if syslog-ng is running as a daemon in the background.

Before you begin

The Python template function was part of syslog-ng since version 3.10, but the logging to internal() was implemented only in 3.20. If your operating system of choice uses an earlier version, or does not feature Python support at all, check our syslog- ng binary packages page.

Configuration and code

We will cover configuration and code in smaller chunks later on, but here you can see it as a whole. And it is also better for your copy & paste convenience. It is a complete working configuration customized to test the Python template function. You can replace your syslog-ng.conf with it, or (just as I did) start syslog-ng by giving this configuration as an argument.

@version: 3.20

source s_internal { internal(); };
destination d_internal { file("/var/log/internal.txt"); };
log {source(s_internal); destination(d_internal); };

python {
import socket
import syslogng

logger = syslogng.Logger()

def resolve_host(log_message, ipaddr):
    logger.trace("Preparing to resolve " + str(ipaddr))
    try:
        hostname = socket.gethostbyaddr(ipaddr)[0]
        logger.trace("Successfully resolved " + str(ipaddr))
        return hostname
    except (socket.herror, socket.error):
        logger.info("Failed to resolve " + str(ipaddr))
        return 'unknown'
};

destination d_local {
    file(
        "/var/log/resolve.txt"
        template("${ISODATE} Host: $(python resolve_host ${MESSAGE}) IP: ${MESSAGE}\n")
    );
};

log {
  source { tcp(port(5555)); };
  destination(d_local); 
};

Now lets check the above configuration more in depth.

@version: 3.20

As usual, the configuration starts with a version number. Version 3.20 of syslog-ng is the first one to include the new Python logger possibilities.

source s_internal { internal(); };
destination d_internal { file("/var/log/internal.txt"); };
log {source(s_internal); destination(d_internal); };

The new logger() method sends log messages to the internal() source of syslog-ng. In the configuration snippet above we declare a source for internal messages, a file destination and connect the two using a log statement.

python {
import socket
import syslogng

logger = syslogng.Logger()

def resolve_host(log_message, ipaddr):
    logger.trace("Preparing to resolve " + str(ipaddr))
    try:
        hostname = socket.gethostbyaddr(ipaddr)[0]
        logger.trace("Successfully resolved " + str(ipaddr))
        return hostname
    except (socket.herror, socket.error):
        logger.info("Failed to resolve " + str(ipaddr))
        return 'unknown'
};

The Python code above is enclosed in a Python block within the configuration. It imports syslogng and creates a logger instance that we will use to send diagnostic and debug log messages to the internal() destination of syslog-ng.

Next the resolve_host method is defined, which receives an IP address as parameter next to the mandatory log message object. It returns a host name when it can be resolved from the IP address and “unknown” when it fails.

As you can see from the related PR, logger supports different log levels. In the above code we use “trace” for messages only used for debugging, which are only shown or saved when trace level is explicitly enabled with the -t command line parameter or using syslog-ng-ctl. When resolving the IP address to host name does not work for some reason (either because the parameter is not a valid IP address or it can not be resolved), an info level message is sent to the internal() destiantion.

destination d_local {
    file(
        "/var/log/resolve.txt"
        template("${ISODATE} Host: $(python resolve_host ${MESSAGE}) IP: ${MESSAGE}\n")
    );
};

In the file destination above we use the Python template function to resolve IP addresses to host names. As you can see, the method name is the same as in the code above. It expects that the message part of the log contains only the IP address and it sends it as a parameter to the method. In the actual log message written to disk the Python template function is replaced with the return value of the log_message method.

log {
  source { tcp(port(5555)); };
  destination(d_local); 
};

The log path above connects a very simple TCP source with the destination that uses the Python template.

Testing

First start syslog-ng in the foreground with all the debugging options enabled, including trace:

syslog-ng -f /etc/syslog-ng/conf.d/py_template.conf -Fvdet

This prints pages of information on screen about the loading of modules and initialization of the configuration. Once syslog-ng is up and running open a second terminal and send a message using the logger command to the port defined in the configuration:

logger -n 127.0.0.1 -T -P 5555 --rfc3164 192.188.243.132

This is the primary IP address I used during my university years, and one of the two public IP addresses I still know from the top of my head. You can use anything else, as long as it can be resolved to a host name.

On screen you will see many messages, including trace level messages coming from the Python code:

[2019-04-02T09:46:08.426631] Syslog connection accepted; fd='13', client='AF_INET(127.0.0.1:58072)', local='AF_INET(0.0.0.0:5555)'
[2019-04-02T09:46:08.427341] Incoming log entry; line='<13>Apr  2 09:46:08 localhost root: 192.188.243.132'
[2019-04-02T09:46:08.427387] Initial message parsing follows;
[2019-04-02T09:46:08.427403] Setting value; name='PROGRAM', value='root', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427407] Setting value; name='LEGACY_MSGHDR', value='root: ', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427409] Setting value; name='HOST', value='localhost', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427411] Setting value; name='MESSAGE', value='192.188.243.132', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427417] >>>>>> Source side message processing begin; instance='tcp,127.0.0.1', location='/etc/syslog-ng/conf.d/py_template.conf:32:12', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427661] Setting value; name='HOST_FROM', value='localhost', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427667] Setting value; name='HOST', value='localhost', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427670] Setting value; name='SOURCE', value='#anon-source0', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427693] Initializing destination file writer; template='/var/log/resolve.txt', filename='/var/log/resolve.txt'
[2019-04-02T09:46:08.427749] affile_open_file; path='/var/log/resolve.txt', fd='15'
[2019-04-02T09:46:08.427766] <<<<<< Source side message processing finish; instance='tcp,127.0.0.1', location='/etc/syslog-ng/conf.d/py_template.conf:32:12', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427771] EOF occurred while reading; fd='13'
[2019-04-02T09:46:08.427825] Syslog connection closed; fd='13', client='AF_INET(127.0.0.1:58072)', local='AF_INET(0.0.0.0:5555)'
[2019-04-02T09:46:08.427852] Closing log transport fd; fd='13'
[2019-04-02T09:46:08.427905] $(python): Invoking Python template function; function='resolve_host', msg='0x7ff1e0015fc0'
[2019-04-02T09:46:08.427918] Preparing to resolve b'192.188.243.132';
[2019-04-02T09:46:08.429410] Successfully resolved b'192.188.243.132';
[2019-04-02T09:46:08.429455] Outgoing message; message='2019-04-02T09:46:08+02:00 Host: kom2.neptun.szie.hu IP: 192.188.243.132\x0a'

Now stop syslog-ng using ^C and check your log messages. You will have a little surprise:

[root@localhost ~]# cat /var/log/resolve.txt 
2019-04-02T09:46:08+02:00 Host: kom2.neptun.szie.hu IP: 192.188.243.132
[root@localhost ~]# cat /var/log/internal.txt
cat: /var/log/internal.txt: No such file or directory

There is no file for your internal logs. The reason is simple: when you request your debug logs printed on the terminal, then these logs are not sent to internal().

Next start syslog-ng in the background without the debugging options:

syslog-ng -f /etc/syslog-ng/conf.d/py_template.conf

Now you do not see anything on screen and receive back the command prompt immediately. You can start sending IP addresses as log messages to syslog-ng again. First I send a valid IP then one which can not be resolved:

[root@localhost ~]# logger -n 127.0.0.1 -T -P 5555 --rfc3164 192.188.243.134
[root@localhost ~]# logger -n 127.0.0.1 -T -P 5555 --rfc3164 192.188.243.1345

We can now stop syslog-ng and check the logs:

[root@localhost ~]# syslog-ng-ctl stop
Shutdown initiated
[root@localhost ~]# cat /var/log/resolve.txt 
2019-04-02T09:46:08+02:00 Host: kom2.neptun.szie.hu IP: 192.188.243.132
2019-04-02T10:07:49+02:00 Host: iktatas.szie.hu IP: 192.188.243.134
2019-04-02T10:07:52+02:00 Host: unknown IP: 192.188.243.1345
[root@localhost ~]# cat /var/log/internal.txt
Apr  2 10:05:31 localhost syslog-ng[2333]: syslog-ng starting up; version='3.20.1'
Apr  2 10:07:49 localhost syslog-ng[2333]: Syslog connection accepted; fd='5', client='AF_INET(127.0.0.1:58092)', local='AF_INET(0.0.0.0:5555)'
Apr  2 10:07:49 localhost syslog-ng[2333]: Syslog connection closed; fd='5', client='AF_INET(127.0.0.1:58092)', local='AF_INET(0.0.0.0:5555)'
Apr  2 10:07:52 localhost syslog-ng[2333]: Syslog connection accepted; fd='5', client='AF_INET(127.0.0.1:58094)', local='AF_INET(0.0.0.0:5555)'
Apr  2 10:07:52 localhost syslog-ng[2333]: Syslog connection closed; fd='5', client='AF_INET(127.0.0.1:58094)', local='AF_INET(0.0.0.0:5555)'
Apr  2 10:07:52 localhost syslog-ng[2333]: Failed to resolve b'192.188.243.1345';
Apr  2 10:08:34 localhost syslog-ng[2333]: syslog-ng shutting down; version='3.20.1'

You can see my second university IP address resolved, and also “unknown” when I introduced a typo into the IP address.

In the internal logs you can see the startup and shutdown messages and entries about network connections. Even if we sent two IP addresses to be resolved by the Python template, there is only one log. As we do not have trace logging enabled, those log messages are not included here, only the one related to the failure.

Learn more about syslog-ng and Python

If you are interested in extending syslog-ng in Python, check our blog posts about using Python with syslog-ng.

You can also join us on a journey to explore the various ways syslog-ng can help bridge the gap between the cloud and on-premises resources with custom integrations built using syslog-ng’s Python API.

If you have questions or comments related to syslog-ng, do not hesitate to contact us. You can reach us by email or you can even chat with us. For a list of possibilities, check our GitHub page under the “Community” section at https://github.com/balabit/syslog-ng. On Twitter, I am available as @PCzanik.

Related Content