Logging mit Docker und Elasticsearch

Der Elastic Stack

Um die Log-Analyse effizient durchführen zu können, gibt es Tools, die Logs von verschiedenen Quellen einsammeln, aufbereiten, indizieren und ein Frontend zur Suche anbieten. Die bekanntesten Tools sind Splunk und der sogenannte Elastic Stack (früher: ELK Stack). Letzterer ist aufgrund der Leistungsfähigkeit und der freien Verfügbarkeit der Komponenten (Apache License 2.0) sehr populär.

Der Elastic Stack besteht aus den folgenden Komponenten:

  • Elasticsearch, eine Datenbank und Suchmaschine, die Daten (hier: Log-Einträge) skalierbar speichert und indiziert.
  • Kibana, eine UI, um auf die Daten in Elasticsearch bequem zugreifen zu können.
  • Logstash, eine Komponente, um Daten einzusammeln, transformieren und anzureichern.
  • Beats, ein Daten-Collector. Es gibt mit Filebeat, Metricbeat, Packetbeat etc. verschiedene spezialisierte Implementierungen der Beats-API.

Logstash ist zwar ein flexibles und mächtiges Tool, gilt aber als durchaus ressourcenintensiv. Daher hat Elastic mit Beats einen Daten-Collector komplett neu entwickelt, der ausschließlich Daten einsammelt und weiterleitet.

Elastic Stack, mit und ohne Logstash
Elastic Stack, mit und ohne Logstash

Mit dem Ziel der Komplexitätsreduktion wird im weiteren Setup auf Logstash komplett verzichtet. Filebeat unterstützt einfache Transformationen (z. B. JSON Decoding), die für unseren Anwendungsfall ausreichen. Je nach Last und Anforderung an die Transformation (z. B. Lookups oder Geo-Anreicherung) kann es aber sinnvoll sein, die Transformationslogik in dedizierte Logstash Knoten auszulagern, die dann vor Elasticsearch angesiedelt sind. Als Daten-Collector sollte Logstash aber nicht mehr eingesetzt werden.

Ansätze

Bei der konkreten Umsetzung gibt es verschiedene Ansätze, wie die Anwendungslogs nach Elasticsearch gesendet werden.

Wir gehen davon aus, dass Elasticsearch korrekt aufgesetzt und sinnvoll konfiguriert ist.

Die fachliche Anwendung ist eine Spring Boot Anwendung, die mit Logback per Logstash JSON Encoder (wichtig!) loggt und in mehreren Docker Containern läuft. Zur Ausfallsicherheit und für ein Zero-Downtime-Deployment werden die Container zudem auf mindestens zwei Docker Hosts ausgeführt.

1. Log Appender

Der zunächst einfachste Ansatz ist die Konfiguration eines spezialisierten Logback Elasticsearch Appenders, der direkt nach Elasticsearch schreibt.

Log Appender
Log Appender

Dabei werden die Log-Events asynchron (d. h. in einem eigenen Thread) an Elasticsearch geschickt.

Dieser Ansatz hat jedoch einige Nachteile:

  • Die Anwendung hat eine Laufzeitabhängigkeit zum Log-Aggregator. Wenn Elasticsearch nicht erreichbar ist, läuft irgendwann die Backlog Queue (Default: 200 MB) voll und Log Events gehen verloren.
  • Der Appender ist Teil der Anwendung. Wenn die Anwendung heruntergefahren wird oder der Container stirbt werden noch nicht verarbeitete Log-Events (die bei einem Crash besonders wichtig sind) nicht mehr verarbeitet.
  • Ausgaben, die vor dem Start des Appenders oder des Spring Contexts geschrieben werden und ggf. Fehlerursachen werden nicht verarbeitet.
  • Die Konfiguration der Elasticsearch Anbindung erfolgt direkt in der Anwendung. Durch diese enge Kopplung müssen bei einer Konfigurationsänderung alle Container neu deployed werden.

Ein entsprechender Appender ist also für die Produktion in der Regel nicht zu empfehlen.

2. Sidecar Container mit Shared Volume

Aus dem Kubernetes-Umfeld kommt das Konzept des Sidecar Containers, um technische Aspekte zu entkoppeln. Bei diesem Ansatz werden Logs ganz klassisch in ein Log-File geschrieben, was (mit einer entsprechenden Rotation Policy) sehr zuverlässig funktioniert. Klassische Log-Files haben den Vorteil, dass sie im Notfall direkt geöffnet und z. B. mit grep durchsucht werden können, auch wenn der Log-Aggregator ausfällt.

Die Log-Verarbeitung wird mit Filebeat in einem zweiten Container (sog. Sidecar-Container) ausgeführt. Log-Einträge können auch dann noch verarbeitet werden, wenn der Anwendungscontainer stirbt. Um Zugriff auf die Log-Files zu haben, wird ein gemeinsames Docker Volume verwendet.

Sidecare Container mit Shared Volume
Sidecare Container mit Shared Volume

Der Ansatz hat ebenfalls einige Aspekte, die zu beachten sind:

  • Wichtige Informationen zur Laufzeitumgebung fehlen, die innerhalb des Containers nicht verfügbar sind. Dazu gehören unter anderem Host, Instance-ID, Image-Name, Image-Version und Docker-Version des Anwendungscontainers.
  • Es stehen keine Logs der Docker-Engine zur Verfügung, z. B. dass ein Container nicht gestartet werden konnte.
  • Die Anwendung muss sich um die Logging-Konfiguration, wie File-Appender und Log-Rotation selber kümmern.

Variante: In Kubernetes wird der Side-Car Container typischerweise innerhalb des Pods gestartet. Bei Deployment auf einem Docker Hosts können auch mehrere Anwendungscontainer in das gleiche Volume schreiben. Dann reicht ein Filebeat-Container, um die Logs zu verarbeiten. Wichtig ist dann natürlich, dass alle Container in unterschiedliche Log-Pfade schreiben. Außerdem muss der Name der Komponente Teil der Log-Eintrags sein. Mein Kollege Eberhard Wolff verwendet diese Lösung in seiner Microservices Demo.

3. Docker JSON File Logging Driver mit Filebeats auf Docker Host

Docker loggt standardmäßig über den JSON File Logging Driver alle Ausgaben von stdout und stderr eines Containers in eine Log-Datei als JSON-Einträge (⚠️ per Default ohne Log-Rotation, das sollte man in der Docker Engine dringend aktivieren).

Die Docker Log-Files liegen unter Linux in /var/lib/docker/containers/<container-id>/<container-id>-json.log.

Wunderbar, denn das können wir nutzen, um per Filebeat die Docker-Logs einzusammeln, um wichtige Docker-Metadaten anzureichern und an Elasticsearch zu schicken. Filebeat wird auf jedem Docker Host installiert.

Die Anwendung loggt nun über den Console-Appender einfach nur nach stdout. Bei Spring Boot ist das die Standard-Einstellung.

Docker JSON File Logging Driver mit Filebeats auf Docker Host
Docker JSON File Logging Driver mit Filebeats auf Docker Host

Damit ist das Single Responsibility Prinzip erfüllt: Die Anwendung muss keine Details zur Logging-Architektur kennen und sich nicht um die Organisation von Log-Files kümmern. Filebeat ist einzig dafür verantwortlich die Log-Einträge an Elasticsearch zu schicken.

Insgesamt schon ganz gut. Zu bewerten gilt:

  • Filebeat muss direkt auf dem Betriebssystem des Docker Hosts installiert sein.
  • Die Logging-Architektur hat Auswirkungen auf die zugrunde liegende Infrastruktur. Damit geht entsprechende Flexibilität verloren, die durch die Abstraktion mittels Container erreicht werden sollte.

4. Docker JSON File Logging Driver mit Filebeats als Docker Container

Als Alternative kann Filebeat auch in einem Filebeat-Container ausgeführt werden. Der Container muss dann auf jedem Docker Host einmal laufen.

Docker JSON File Logging Driver mit Filebeats als Docker Container
Docker JSON File Logging Driver mit Filebeats als Docker Container

Per Docker Bind Mount werden dazu die Verzeichnisse /var/lib/docker (die Docker Log Files) und /var/run/docker.sock (Zugriff auf die Docker Metadaten) eingebunden.

Das Single Responsibility Prinzip ist natürlich auch hier erfüllt und die Log-Verarbeitung läuft isoliert in einem Container.

Die optimale Lösung also? Ja! Ein paar Dinge müssen aber noch beachtet werden:

  • Filebeat speichert den Zustand, welche Daten schon verarbeitet wurden, in einer Registry Datei. Diese Registry Datei sollte in einem Volume persistiert werden, damit bei einen Container Rebuild nicht alle Log-Files erneut verarbeitet werden. Elasticsearch würde Duplikate zwar erkennen, aber die unnötige Last sollte vermieden werden.
  • Die oben genannten Bind Mounts werden als Owner root eingebunden, Filebeat muss also entsprechende Zugriffsrechte haben.
  • Das offizielle Filebeat Docker Image loggt selbst nach stderr (Switch -e). Die Einträge landen demnach ebenfalls in den Docker-Logs und die werden wieder von Filebeats gelesen. Wenn die Verarbeitung dieses Eintrages wiederum zu einem Fehler führt, führt dies zu einer Endlosschleife. Um das zu verhindern, kann z. B. Filebeat so konfiguriert werden, dass in eine lokale Datei geloggt wird.

5. Docker Fluentd Logging Driver

Docker ermöglicht die Konfiguration spezialisierter Logging Driver. Schauen wir uns diese näher an. Als Beispiel nehmen wir Fluentd, die anderen Logging Driver funktionieren aber ähnlich.

Fluentd ist ein universeller und verbreiteter Log-Collector. Docker bietet einen entsprechenden Fluentd Logging Driver an. Der wesentliche Unterschied zu den Filebeat-Ansätzen besteht darin, dass die Docker-Engine die Log-Einträge nicht in eine Datei schreibt, sonder direkt an einen Fluentd Agent streamt.

Dessen Aufgabe des Agents ist das Weiterleiten der Log-Einträge an einen Log-Aggregator. Die Anbindung an Elasticsearch ist z. B. über ein Plugin möglich.

Docker Fluentd Logging Driver
Docker Fluentd Logging Driver

Der Fluentd Agent wird entweder auf dem Docker Host installiert (analog Ansatz 3), oder in einem Container ausgeführt (analog Ansatz 4). Da der Docker Logging Driver eine direkte Laufzeitabhängigkeit zum Fluentd Agent hat, ist tendenziell die Installation direkt auf dem Docker Host zu empfehlen.

Es handelt sich ebenfalls um eine verbreitete Lösung, aus meiner Sicht aber mit einigen Nachteilen, die sich aufgrund des Streamings ergeben:

  • Direkte Laufzeit-Abhängigkeit zwischen Docker-Engine und Agent. Wenn der Agent nicht verfügbar ist, startet der Docker-Container nicht. Es gibt zwar eine Option, den Container trotzdem zu starten, aber die Gefahr ist dann, dass ein Konfigurationsfehler nicht auffällt.
  • Wenn Elasticsearch nicht verfügbar ist, hat der Log-Agent nur einen beschränkten Puffer (einige hundert MB). Wenn der Puffer vollläuft oder der Prozess abstürzt, gehen Einträge verloren.
  • Das Setup beinhaltet eine weitere Komponente von einem anderen Hersteller.
  • Eine lokale Log-Analyse per docker logs <container-Id> ist nicht mehr verfügbar.

Wenn Fluentd noch nicht im Einsatz ist, würde ich daher von dieser Lösung eher abraten und zu den Optionen 3 oder 4 mit Filebeat tendieren.

Filebeat Konfiguration

OK, wie muss Filebeat in den Optionen 3 oder 4 konfiguriert werden, um die Anwendungslogs aus den Docker-Logs zu extrahieren?

Schauen wir uns einen Docker Log-Eintrag an:

{"log":"{\"@timestamp\":\"2018-01-11T13:23:50.506+00:00\",\"@version\":1,\"message\":\"Hello World\",\"logger_name\":\"com.example.demo.DemoApplication\",\"thread_name\":\"http-nio-8080-exec-1\",\"level\":\"INFO\",\"level_value\":20000,\"X-Span-Export\":\"false\",\"X-B3-SpanId\":\"63cd343d9e5beb85\",\"X-B3-TraceId\":\"63cd343d9e5beb85\"}\n","stream":"stdout","time":"2018-01-11T13:23:50.508292211Z"}

Der fachliche JSON Log-Eintrag ist im Feld log als String encodiert. Ziel ist natürlich, dass auch die Informationen aus den fachlichen Log-Informationen (wie message und level) als Elasticsearch Felder erfasst und indiziert werden. Es ist also eine Transformation (JSON-Decoding) notwendig, um den String in JSON zu verwandeln. Mit dem Processor json_decode_fields kann genau dies mit Bordmitteln erreicht werden.

Hier eine vollständige Filebeat Konfigurationsdatei:

#filebeat.yml
filebeat.prospectors:
-type: docker
  paths:
   -'/var/lib/docker/containers/*/*.log'
  containers.ids: '*'

processors:
  -decode_json_fields:
      fields: ["message"]
      target: ""
      overwrite_keys: true
  -add_docker_metadata: ~

output.elasticsearch:
  hosts: ["elasticsearch:9200"]

logging.to_files: true
logging.to_syslog: false
filebeat.yml

Der Log-Eintrag wird nun nach Elasticsearch geschickt, um Docker Metadaten ergänzt und alle fachlichen Log-Felder wurden dekodiert:

{
  "_index": "filebeat-6.1.1-2018.01.11",
  "_type": "doc",
  "_id": "cBpk5WABPvdYx84ciqFQ",
  "_score": 1,
  "_source": {
    "@timestamp": "2018-01-11T13:23:50.506Z",
    "offset": 23189,
    "log": "",
    "X-B3-SpanId": "63cd343d9e5beb85",
    "level_value": 20000,
    "@version": 1,
    "X-B3-TraceId": "63cd343d9e5beb85",
    "prospector": {
      "type": "docker"
    },
    "docker": {
      "container": {
        "id": "8132c747afd979939c1765714eee417d6674f18e0cc20c3aa632774129d9f114",
        "image": "demo",
        "name": "awesome_bell"
      }
    },
    "source": "/var/lib/docker/containers/8132c747afd979939c1765714eee417d6674f18e0cc20c3aa632774129d9f114/8132c747afd979939c1765714eee417d6674f18e0cc20c3aa632774129d9f114-json.log",
    "message": "Hello World",
    "level": "INFO",
    "beat": {
      "name": "24282b13a2ca",
      "hostname": "24282b13a2ca",
      "version": "6.1.1"
    },
    "stream": "stdout",
    "thread_name": "http-nio-8080-exec-1",
    "logger_name": "com.example.demo.DemoApplication",
    "X-Span-Export": "false"
  },
  "fields": {
    "@timestamp": [
      "2018-01-11T13:23:50.506Z"
    ]
  }
}
Log-Eintrag in Elasticsearch

In Elasticsearch sollte jetzt noch ein Index Template konfiguriert werden, um den Index anzulegen und die notwendigen Felder korrekt zu indizieren. Ein Index Template kann auch direkt in Filebeat konfiguriert werden, wenn Automatic Template Loading aktiviert ist.

Fazit

Mit Ausnahme des Log-Appenders sind alle Ansätze valide.

Im Sinne des Single Responsibility Principles sollte der Aspekt Log-Aufbereitung nicht Teil der fachlichen Anwendung sein. Dazu gehört insbesondere, dass fachliche Komponenten nicht von der Verfügbarkeit eines Log-Aggregators abhängig sind. Wenn der Log-Aggregrator ausfällt oder langsam ist, darf weder die Anwendung davon betroffen sein (Failure-Isolation), noch dürfen Log-Einträge verloren gehen.

Meine Empfehlung ist Variante 4. Docker JSON File Logging Driver mit Filebeats als Docker Container. Dieser Ansatz bietet die größte Entkopplung, enthält wichtige Metainformationen und Filebeat läuft unabhängig vom Host-System in einem eigenen Container. Und: Im Notfall liegen die Log-Files immer noch als klassische Dateien vor.

Source-Code

Github Repository mit Beispiel-Code und einer lauffähigen Demo.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more

Find us on