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:

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:

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:

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:

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:

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:

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 (aktualisiert auf filebeat Version 7.0.0):

### filebeat.yml
filebeat.inputs:
- type: docker
  containers.ids: '*'
  json.message_key: message
  json.keys_under_root: true
  json.add_error_key: true
  json.overwrite_keys: true

processors:
- 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-7.0.0-2019.04.22-000001",
  "_type": "_doc",
  "_id": "DdTvRGoBRbwb0R0qy5ld",
  "_version": 1,
  "_score": null,
  "_source": {
    "@timestamp": "2019-04-22T12:04:58.643Z",
    "agent": {
      "type": "filebeat",
      "ephemeral_id": "71a5ba55-908a-4c39-ac7d-f7d76d19f29d",
      "hostname": "16a7321e8be1",
      "id": "e6f4b5fd-097f-4b69-9442-5a7ecfd5214a",
      "version": "7.0.0"
    },
    "container": {
      "id": "f9d3ff76fee5f8e19e5605bc8bc4180104c4bfc03d8228b5769cf5ce4804328e",
      "image": {
        "name": "docker-logging-elasticsearch-3_demo"
      },
      "name": "docker-logging-elasticsearch-3_demo_1",
      "labels": {
        "com_docker_compose_oneoff": "False",
        "com_docker_compose_project": "docker-logging-elasticsearch-3",
        "com_docker_compose_service": "demo",
        "com_docker_compose_version": "1.23.2",
        "com_docker_compose_config-hash": "49ac15052acde92c22c145362b0c5f0a8d12a371ac69b85e32787db1079f69b8",
        "com_docker_compose_container-number": "1"
      }
    },
    "log": {
      "offset": 22658,
      "file": {
        "path": "/var/lib/docker/containers/f9d3ff76fee5f8e19e5605bc8bc4180104c4bfc03d8228b5769cf5ce4804328e/f9d3ff76fee5f8e19e5605bc8bc4180104c4bfc03d8228b5769cf5ce4804328e-json.log"
      }
    },
    "message": "Hello World",
    "input": {
      "type": "docker"
    },
    "@version": 1,
    "stream": "stdout",
    "logger_name": "com.example.demo.DemoApplication",
    "level_value": 20000,
    "thread_name": "http-nio-8080-exec-1",
    "host": {
      "name": "16a7321e8be1"
    },
    "X-B3-SpanId": "49391f814e3dccec",
    "level": "INFO",
    "X-Span-Export": "false",
    "X-B3-TraceId": "49391f814e3dccec"
  },
  "fields": {
    "suricata.eve.timestamp": [
      "2019-04-22T12:04:58.643Z"
    ],
    "@timestamp": [
      "2019-04-22T12:04:58.643Z"
    ]
  }
}
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.