Logging a Python App to journald

So I thought logging in Python would be easy.

You start logging to the command line then when that gets too much you log to a file. A bit of me still feels logging to a file in the app directory is the easiest set up. But various articles on the Internet say file logging can cause problems when you containerise the app or run it as a service. A better suggested option is to log to journald with other systemd services.

Logging to journald for viewing with journalctl

There is a fair bit of information online on viewing and filtering journald logs using the journalctl command line program.

Writing to journald from Python is fairly simple but the online information is a bit patchy and contradictory. As of 2022 – here is a recipe that works.

First, install systemd development libraries:

sudo apt install libsystemd-dev

Then pip install the python systemd library:

pip install systemd-python

In a high level module __init__.py file I then have the following logging setup:

import logging
import os
from systemd.journal import JournalHandler

# Define module logger
logger = logging.getLogger(__name__)
# Initially set to log all - change this in production
logger.setLevel(logging.DEBUG)

# Create journal logger
journalHandler = JournalHandler(SYSLOG_IDENTIFIER='my_app_name')

# create formatter - can also use %(lineno)d -
# see https://stackoverflow.com/questions/533048/how-to-log-source-file-name-and-line-number-in-python/44401529
formatter = logging.Formatter(
    '%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s | %(filename)s > %(module)s > %(funcName)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# add formatter to jh
journalHandler.setFormatter(formatter)

# add jh to logger
logger.addHandler(journalHandler)

# Extra bit to get logging level from environment variable
DEBUG_MODE = (os.environ.get('DEBUG_MODE', 'False') == 'True')
if DEBUG_MODE:
    logger.setLevel(logging.DEBUG)
else:
    logger.setLevel(logging.INFO)

Viewing with journalctl

To view the journald logs we have two options:

  • command line viewing via journalctl (good for ssh access to remote servers)
  • simple GUI via QJournalctl

As an example, you can use journalctl to filter based on the SYSLOG_IDENTIFIER:

journalctl SYSLOG_IDENTIFIER=my_app_name

To view streaming logs just add the follow – -f – flag:

journalctl SYSLOG_IDENTIFIER=my_app_name -f

To filter logs below a certain level, use the priority – -p – flag. Level 6 is INFO, level 7 is DEBUG. So the following will filter out DEBUG messages:

journalctl SYSLOG_IDENTIFIER=my_app_name -p 6

Logging with Celery

I use celery to manage a backend task queue. Celery uses its own logging arrangements. I found it useful to also direct this to journald so I could see everything with the same log views. I followed the approach helpfully described here.

In your celery module use the after_setup_logger signal to tag on your configured logging setup:

from celery.signals import after_setup_logger

from app import logger as app_logger, journalHandler


# ... other celery app configure ... 


# Setup logging by augmenting the celery logger as per: https://www.distributedpython.com/2018/08/28/celery-logging/
@after_setup_logger.connect
def setup_loggers(logger, *args, **kwargs):
    """Setup logging for celery."""
    # Add journal handler to celery logger
    logger.addHandler(journalHandler)

The celery app then logged to journald as well.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s