Logging von Docker-Containern mit journald

Oft etwas vernachlässigt, bietet das Logging eine Möglichkeit, das diffuse Bild einer Container-Applikation aufzuklaren. Der generische Ansatz von Docker hilft dabei unterschiedlichsten Applikationen gerecht zu werden. Nach einer kurzen Einführung in das Docker-Logging soll dieser Artikel eine Anbindung an journald erläutern.

Starten der Demo-Applikation

Um ein Gefühl für Logging mit Docker zu erhalten, benötigen wir zunächst einen Applikationscontainer. Ein nginx, der als Webserver fungiert und im schlanken Alpine-Gewand daherkommt, erfüllt diesen Zweck hervorragend. Auf einem Linux-Host starten wir also den Container …

$ docker container run -p 80:80 -d --name=web nginx:1.12-alpine

docker logs

Anschließend rufen wir im Browser die Demo-Seite ein paar mal auf, da nginx jeden Aufruf protokolliert. Die Logs des Containers rufen wir über den Namen oder die ID ab. In unserem Fall mit docker logs web. Zusätzlich gibt es noch eine Handvoll Flags für den Befehl, um beispielsweise genauere Timesstamps zu erhalten oder die Ergebnismenge einzugrenzen. Dies ist die Ausgabe der einfachen Befehlsform:

127.0.0.1 - - [21/Jul/2017:17:04:09 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
127.0.0.1 - - [21/Jul/2017:17:04:10 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
127.0.0.1 - - [21/Jul/2017:17:04:11 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"

Docker sammelt grundsätzlich alles, was die Applikation im Container in stdout und stderr schreibt. Im offiziellen Docker-Image des nginx linken die Standard-Logdateien „access.log“ und „error.log“ nach /dev/stdout und /dev/stderr, wie dem Dockerfile zu entnehmen ist. Ein Hand-Anlegen ist also nicht notwendig.

Doch schauen wir uns den Container mit docker container inspect web doch mal genauer an:

[
    {
        // [...]
        "LogPath": "/var/lib/docker/containers/0c6f45-gekuerzt-json.log",
        // [...]
        "HostConfig": {
            // [...]
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
        },
        // [...]
    }
]

Unter dem angegebenen LogPath werden die von Docker gesammelten Logs auf die Festplatte geschrieben. Zudem wird angezeigt, welcher Logging Driver für den verwendeten Container im Einsatz ist. Standardmäßig ist das json-file.

Ruft man diese mit cat /var/lib/docker/containers/0c6f45-gekuerzt-json.log direkt auf, sieht das formatiert so aus:

{
  "log": "127.0.0.1 - - [21/Jul/2017:17:04:10 +0000] \"GET / HTTP/1.1\" 304 0 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\" \"-\"\n",
  "stream": "stdout",
  "time": "2017-07-21T17:04:09.625609119Z"
}
{
  "log": "127.0.0.1 - - [21/Jul/2017:17:04:11 +0000] \"GET / HTTP/1.1\" 304 0 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\" \"-\"\n",
  "stream": "stdout",
  "time": "2017-07-21T17:04:10.591183632Z"
}
{
  "log": "127.0.0.1 - - [21/Jul/2017:17:04:12 +0000] \"GET / HTTP/1.1\" 304 0 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\" \"-\"\n",
  "stream": "stdout",
  "time": "2017-07-21T17:04:11.134125994Z"
}

Wir erhalten einen deutlich tieferen Blick in Dockers Logging-Mechanismus und sehen zusätzlich noch aus welchem stream (stdout/stderr) der Log-Eintrag stammt und erhalten nun auch genaueren Zeitstempel. nginx schreibt übrigens beim Aufruf eines nicht vorhandenen Pfads, also bei einem Fehler 404, in stderr.

Logging Driver

Docker bringt von Hause eine Vielzahl von Logging Drivers mit. Neben dem Einsatz einer JSON-Datei können Logs beispielsweise zum syslog Daemon des Host-Systems übergeben werden. Das Graylog Extended Log Format, kurz gelf, kann gewählt werden, um die Log-Einträge an einen Graylog-Server oder Logstash zu übertragen. Produkte der bekannten Cloud-Provider sind natürlich ebenfalls nutzbar.

Ich möchte mich jedoch zunächst mit dem Treiber für journald beschäftigen. journald kommt zusammen mit dem Init-System systemd und ist bei diversen Linux Distributionen vorinstalliert. Als dedizierter Logging-Dienst beherrscht er unter anderem Logrotation, was json-file fehlt.

Die Zuweisung eines Logging Drivers kann auf vielen verschiedenen Ebenen stattfinden.

# Definition am Container
$ docker container run --log-driver journald --log-opt env=SOME_ENV_VAR [...]

# Definition am Service
$ docker service create --log-driver journald --log-opt env=SOME_ENV_VAR [...]

# Definition am Service im Docker Compose File
$ cat docker-compose.yml
version: "3"

services:
  web:
    image: nginx:1.12-alpine
    ports:
      - "80:80"
    logging:
      driver: "journald"
      options:
        env: "SOME_ENV_VAR"

An den Beispielen sieht man, dass je nach Log-Treiber auch noch Optionen übergeben werden können, wie Zugangsdaten zu einem externen Dienst. Welche das sind, erfährt man in der ausführlichen Dokumentation.

Logging im Docker Daemon

Neben den oben gezeigten Möglichkeiten zur Definition eines Logging-Treibers auf sehr kleinteiliger Ebene, können wir auch einen generellen Treiber im Docker Daemon bestimmen, der dann für die gestarteten Container genutzt wird. Laut Dokumentation wird nach dieser Config-Datei geschaut: /etc/docker/daemon.json. Unter Debian kann ich das nicht bestätigen. Hier ist noch ein zusätzlicher Kniff notwendig, aber der folgt nach dem Inhalt der JSON-Datei.

$ cat > /etc/docker/daemon.json << EOF
{
  "log-driver": "journald"
}
EOF

Unter Debian, und ich vermute auch Ubuntu, müssen wir dem Docker Daemon noch klarmachen, dass er die Config-Datei beim Start hinzuziehen soll. Wir erweitern daher das Docker-Profil in systemd und starten den Dienst anschließend neu.

$ mkdir -p /etc/systemd/system/docker.service.d
$ cat > /etc/systemd/system/docker.service.d/dockerd.conf << EOF
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --config-file /etc/docker/daemon.json
EOF
$ systemctl daemon-reload
$ service docker restart

In der Ausgabe von docker info taucht nun Folgendes auf: Logging Driver: journald

Verwendung von journald

Docker ermöglicht nun weiterhin die Nutzung von docker logs web, da Docker mit journald umzugehen weiß, allerdings lassen sich die Logs nun auch mit dem journalctl abrufen. Beispielsweise so:

$ journalctl CONTAINER_NAME=webserver

Detailliert auf journald einzugehen, würde an dieser Stelle zu weit führen, weshalb ich auf die Dokumentation von journald und journalctl und den Eintrag zum Docker Logging Driver verweisen möchte.

Fazit

Zusammen mit den Healthchecks, die Aufschluss über den Gesundheitszustand unserer App geben und dem Monitoring, also dem Sammeln von Metriken, z. B. mit Prometheus, stellt das Logging ein essentielles Werkzeug zur Überprüfung dar. journald ist dabei nur eine von vielen verschiedenen Möglichkeiten, die Log-Einträge der Applikationscontainer zu sammeln. Gemessen am verwendeten Standard Logging Driver json-file, der als Basis-Einstellung zu verstehen ist, hat sich journald allerdings im Linux-Umfeld einen Namen gemacht und findet sich daher in diversen Linux Distros, wo es an die eigenen Anforderungen, wie Logrotation und Per­sis­tenz, angepasst werden kann. Es lohnt sich daher dem Thema Logging etwas mehr Aufmerksamkeit zu schenken.

Patrick Baber

Als erfahrener Programmierer löst Patrick jeden Gordischen Knoten, findet die geeignete Methode und entwickelt damit flexible Software-Systeme.