Python's stdout buffering and systemd

Running a Python script as a Systemd service

This is very simple, first we need to create a systmed "unit", in /etc/systemd/system/foo.service

[Unit]
Description=Abracadabra
After=network.target

[Service]
Type=simple
User=bob
Restart=on-failure
RestartSec=3
ExecStart=/usr/bin/python3 /home/bob/test_daemon.py
[Install]
WantedBy=multi-user.target

Then, we can enable and start the service with

systemctl enable foo.service
systemctl start foo

What happens to the stdout and stderr?

They can be checked by calling journalctl -u foo. Assuming test_daemon.py was:

from datetime import datetime
from time import sleep
import sys

print("Started abracadabra")

while True:
    curtime = datetime.now().strftime('%X')
    print("Hi! Current time is: ", curtime)
    sleep(2)

we should see

$ sudo journalctl -u foo
Okt 09 21:07:18 etna python3[13499]: Started abracadabra
Okt 09 21:07:18 etna python3[13499]: Hi! Current time is:  21:07:18
Okt 09 21:07:20 etna python3[13499]: Hi! Current time is:  21:07:20
Okt 09 21:07:22 etna python3[13499]: Hi! Current time is:  21:07:22
Okt 09 21:07:24 etna python3[13499]: Hi! Current time is:  21:07:24
Okt 09 21:07:26 etna python3[13499]: Hi! Current time is:  21:07:26
Okt 09 21:07:28 etna python3[13499]: Hi! Current time is:  21:07:28
Okt 09 21:07:30 etna python3[13499]: Hi! Current time is:  21:07:30

Redirecting stdout/stderr to a file

We can ask Systemd to do this by adding these lines in /etc/systemd/system/foo.service

[Service]
---snip---
ExecStart=/usr/bin/python3 /home/bob/test_daemon.py
StandardOutput=file:/var/log/foo/stdout
StandardError=file:/var/log/foo/stderr
---snip---

But nothing gets written to /var/log/foo/stdout!

This is because python buffers stdout if it is not a console. If we really want to see the stdout of our daemon as soon as they are printed, we need to disable this buffering. This can be achieved by starting python with -u flag. So, the systemd unit file changes to:

[Unit]
Description=Abracadabra
After=network.target

[Service]
Type=simple
User=bob
Restart=on-failure
RestartSec=3
ExecStart=/usr/bin/python3 /home/bob/test_daemon.py
StandardOutput=file:/var/log/foo/stdout
StandardError=file:/var/log/foo/stderr
[Install]
WantedBy=multi-user.target

That's it! Now /var/log/foo/stdout should be populated with whatever test_daemon.py prints.