HTTP Application Logging in AL2023

by Sebastien Mirolo on Mon, 14 Apr 2025

With CentOS gone for all intended purposes and Amazon Linux 2 (AL2) being scheduled for the chopping block, we migrated the infrastructure to Amazon Linux 2023 (AL2023). The last remaining part of the migration was the logging system.

Logging the HTTP request pipeline

We are running a typical Nginx / Gunicorn / Django HTTP request pipeline, with the Gunicorn / Django application running as a Docker container.

Nginx logs HTTP requests to files. Those files are rotated and uploaded to the logging storage on a regular basis. We are using OpenTelemetry to fast-track errors.

Gunicorn / Django logs to stdout and stderr. Docker is configured to send logs to journald. Journald forwards events to syslog so we are able to write logs to standard text files, thus using the same aggregator pipeline we do for Nginx logs.

Here is a summary of the important config files

/etc/systemd/journald.conf
...
ForwardToSyslog=yes
...
/etc/sysconfig/docker
...
OPTIONS='--selinux-enabled --log-driver=journald --log-opt tag="{{.Name}}/{{.ID}}"'
/etc/gunicorn.conf
...
errorlog="-"
accesslog="-"
loglevel="info"
access_log_format='%(h)s %({Host}i)s %({User-Session}o)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" "%({X-Forwarded-For}i)s"'
/app/settings.py
...
LOGGING = {
  'formatters': {
    'request_format': {
      'format':
        "%(remote_addr)s %(http_host)s %(username)s [%(asctime)s]"\
        " %(levelname)s %(message)s",
        'datefmt': "%d/%b/%Y:%H:%M:%S %z"
    }
  },
  'handlers': {
    'log': {
      'level': "DEBUG",
      'formatter': "request_format",
      'class': "logging.StreamHandler",
    }
  },
  'loggers': {
...
    # This is the root logger.
    # The level will only be taken into account if the record is not
    # propagated from a child logger.
    #https://docs.python.org/2/library/logging.html#logging.Logger.propagate
    '': {
      'handlers': ["log"],
      'level': "INFO"
    },
  },
}
...

What changed?

We originally configured Logging of Docker containers output through journald to syslog-ng, but with the migration from Amazon Linux 2 to AL2023, syslog-ng is not readily available as a package. There is an experimental AL2023 syslog-ng package. Still at this point we decided to go the rsyslog route.

Terminal
$ sudo dnf install rsyslog

We create a rsyslog config file to separate the application log events into their own file. Note that if the filter doesn't seem to be taken into account, it might be because of interaction with previous rules. We thus use a 00- prefix for the config filename here so it is the first to be loaded by rsyslog.

/etc/rsyslog.d/00-app.conf
# logs app messages in a separate file
if $programname == 'app' then /var/log/gunicorn/app.log

We check there is no syntax error.

Terminal
$ sudo rsyslogd -N1

The expected lines do not always appear in the logfile. It might be a combination of different services needing to be restarted.

Terminal
$ sudo systemctl start rsyslog.service
$ sudo systemctl force-reload systemd-journald
$ sudo systemctl reload rsyslog.service

For debugging, we use a few journalctl commands to track log events through the stack.

Terminal
# tailing output of docker itself (should show messages from all containers)
$ sudo journalctl -l _SYSTEMD_UNIT=docker.service -f

# showing json formatted of all metadata in the output of app container log
$ journalctl --since "5 min ago" CONTAINER_NAME=app -o json-pretty

If the $programname is not set correctly by Docker, or not passed through properly by jounald, it is sometimes easier to start filtering based on the $msg rsyslog property.

At this point, the output of the application makes it to the logfile, but each line is prefixed by sysload information. Since we are interested to have the same output recorded in the logfile as the ones a developer would see when they run python manage.py runserver on the their development machine, we are only going to write the raw message to the logfile.

/etc/rsyslog.d/00-app.conf
# logs app messages in a separate file
template(name="rawMsgFormat" type="string" string="%msg%\n")

if $programname == 'app' then action(type="omfile" file="/var/log/gunicorn/app.log" template="rawMsgFormat")

Et voila! There is no distinction between the logfiles generated in production and the standard output a developer would record on its own development machine.

More to read

You might also like to read Shipping ssh login events through OpenTelemetry, or Fast-tracking server errors to a log aggregator on S3.

More technical posts are also available on the DjaoDjin blog, as well as business lessons we learned running a SaaS application hosting platform.

by Sebastien Mirolo on Mon, 14 Apr 2025


Receive news about DjaoDjin in your inbox.

Bring fully-featured SaaS products to production faster.

Follow us on