Posts Logging to systemd in Python
Post
Cancel

Logging to systemd in Python

One of the common themes in modern Linux is the adoption of systemd. Like it or hate it, it is here to stay.

The main component of logging in systemd is the Journal, controlled by journald. Linux users are undoubtedly familiar with invoking journalctl to view Journal logs. As Python developers that target Linux environments, it isn’t unusual to use systemd to manage our logged events.

I like this approach almost as much as I like logging to stdout, as it is consistent, expected (on Linux), and there is plenty of tooling around to support scraping the logs from systemd to push to central logging, aggregation, etc. (take a look at fluentd and Fluent Bit!).

Installing system dependencies

The Python package that we will rely on is appropriately named systemd. But before we can pip install systemd, we need to first grab system dependencies (specific to the distro we’re using).

On RPM-based distributions (RHEL, CentOS, Fedora) this should be as simple as installing systemd-devel. On my beloved CentOS machines, that would be:

1
# yum install -y systemd-devel

On Debian-based distributions, as per the documentation, the dependencies are build-essential, libsystemd-journal-dev, libsystemd-daemon-dev, and libsystemd-dev.

Once these dependencies are installed, you can then install our necessary Python package:

1
$ pip install systemd

Note: I highly recommend you use a virtual environment for your Python development and deployments (unless containerized), to prevent Python package conflicts. More on this below.

Logging to systemd

Once we have the dependencies installed, logging to systemd is fairly straightforward. I think the best way to show this is with a simple snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import logging
import random
import time
from systemd.journal import JournaldLogHandler

# get an instance of the logger object this module will use
logger = logging.getLogger(__name__)

# instantiate the JournaldLogHandler to hook into systemd
journald_handler = JournaldLogHandler()

# set a formatter to include the level name
journald_handler.setFormatter(logging.Formatter(
    '[%(levelname)s] %(message)s'
))

# add the journald handler to the current logger
logger.addHandler(journald_handler)

# optionally set the logging level
logger.setLevel(logging.DEBUG)

if __name__ == '__main__':
    while True:
        # log a sample event
        logger.info(
            'test log event to systemd! Random number: %s',
            random.randint(0, 10)
        )

        # sleep for some time to not saturate the journal
        time.sleep(5)

The comments above should help explain, but from a higher level we just need to get an instance of a logger (I pass in the module name, but this could’ve been anything). The important part here is to wire up systemd’s Journal by adding a handler to our logger that is of type JournaldLogHandler. I personally like to include the level in my logging messages, so I define a custom formatter for my journald handler.

Virtual environment

Because I rely heavily on virtual environments, I need to make sure systemd sources my virtual environment activation prior to running my Python script as a daemon. This allows me to reap the benefits of a virtual environment (dependency isolation), even while running my code as a daemon.

My usual approach to this is to just create an executable wrapper shell script:

1
2
3
4
5
#!/bin/bash

SCRIPT_PATH=$(dirname "$(realpath "$0")")
. "$SCRIPT_PATH/venv/bin/activate"
python "$SCRIPT_PATH/app.py"

I put this shell script in the same directory as my Python module.

Setting up the systemd unit file and service

Before we can test this out, we need to create a unit file so that systemd knows how to setup and handle our daemon.

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Sample to show logging from a Python application to systemd
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/pysystemdlogging/run_app.sh
Restart=on-abort

[Install]
WantedBy=multi-user.target

Keep this unit file in whatever long term directory you like, typically with the Python source for this application. systemd works heavily off of convention regarding what unit files exist where. Without going into too much depth about systemd (there could be books written, and it is out of the scope of this blog post), we need to make sure our unit file is symlink’d and accessible in /etc/systemd/system:

1
# ln -s “$(pwd)/pylogtosystemd.service” /etc/systemd/system/pylogtosystemd.service

The next step is to reload systemd so that our new unit file is picked up.

1
# systemctl daemon-reload

Verify that the unit file is registered by attempting to get the status of our service.

1
$ systemctl status pysystemdlogging.service

You should see that the service is ‘loaded’ but ‘inactive’. Let’s start our new daemon.

1
# systemctl start pysystemdlogging.service

Viewing the log entries with journalctl

Now that our systemd daemon is up and running, let’s take a look at the entries that are (hopefully) waiting for us in the journal!

1
# journalctl -b -u pysystemdlogging.service

With the “-u” parameter we are able to filter based on a unit (in our case, we only want to see log entries from our daemon). “-b” tells journalctl to only give us entries since the last boot.

Hopefully your output looks similar to mine!

Python and systemd logging output

Summary

I hope this post has illustrated how to easily log to systemd’s Journal from Python! If you’re interested in diving a little more into systemd and unit files, I highly recommend you check out this entry in the Arch wiki. Enjoy!

This post is licensed under CC BY 4.0 by the author.