logging in kubernetes

Logging in Kubernetes mit Fluent Bit und OpenSearch

Logging in Kubernetes dient uns in erster Linie dazu, Prozesse innerhalb eines Containers zu protokollieren. Wir versprechen uns dadurch beispielsweise besseres Auditing, eine schnellere Fehleranalyse und infolgedessen eine robustere Programmierung. Heute wollen wir zeigen, wie es mit dem richtigen Tooling möglich ist, Logs sinnvoll zu persistieren und zu visualisieren.

Was ist Logging?

Bei der Frage, was genau Logging sein soll, sind uns schon allerlei Definitionen und Negativbeispiele untergekommen. Es kommt leider noch viel zu häufig vor, dass in einer Anwendung gar kein Logging betrieben wird oder man sich darauf beschränkt, im Fehlerfall ein Log mit dem gesamten Stacktrace herauszugeben. Eine weitere (schlechte) Methode zu loggen besteht darin, an allen Stellen, an denen jemals etwas passiert ist, ein Log zu setzen und keine Loglevel festzulegen.

Wir würden Logging folgendermaßen definieren:

Logging ist das Protokollieren von Prozessen im Programmablauf. Hierbei werden, je nach Umgebung und Wichtigkeit des Logs, verschiedene Indikatoren (Loglevel) und Informationen im Log bereitgestellt. Ziel sollte es hierbei sein, dass das Log einen Mehrwert liefert und die User nicht mit Informationen überlädt.

Ein Log der keinen nennenswerten Mehrwert liefert, sieht z.B. so aus:

2023/01/04 15:08:39 INFO respond successful

Die einzige Information, die wir diesem Log entnehmen können, ist, dass eine Antwort zu einem API-Call erfolgreich übermittelt wurde.

Das gleiche Log, das uns jedoch einen Mehrwert liefert, könnte so aussehen:

2023/01/04 15:08:39 INFO respond with status 200 for /api/test after 200ms span_id:1234

Hier erhalten wir als Information den Statuscode, die aufgerufene URL, die Zeit, die unsere Applikation zur Beantwortung der Frage benötigt hat, und eine Span ID, mit der wir unseren API-Call mit Tracing analysieren können. Mehr Informationen zu Tracing gibt es hier im Blogartikel.

Pain Points

In einer perfekten Welt würden all unsere Logs wie im zweiten Beispiel aussehen oder, noch besser, sie wären im JSON-Format strukturiert. Da wir in einer verteilten Systemlandschaft wie Kubernetes jedoch meistens mehrere Applikationen betreuen und wir womöglich nur in einem Bruchteil dieser Einfluss auf die Logs haben, kann sich das Lesen und Auswerten der Logs in der Anwendung selbst als aufwendig herausstellen. Auch bei sauberen Logs kann das Auslesen schwierig sein, gerade wenn eine Anwendung viele Anfragen beantwortet und entsprechend Logs generiert. Allerspätestens jetzt sollte man über Tools zum Sammeln, Verarbeiten und Visualisieren der Logs nachdenken.

Mit den richtigen Tools Kubernetes-Logs sammeln und visualisieren

Gewähltes Setup

Damit wir Logs sammeln können, benötigen wir zunächst einen Ort zum Speichern der Daten. Etabliert hat sich hierbei Elasticsearch bzw. OpenSearch. Als UI zum Analysieren und Visualisieren nutzen wir OpenSearch Dashboards. Hier kann man generell jede UI nehmen, die mit dem gewählten Speicher kommunizieren kann, beispielsweise auch ein Grafana, wenn man Prometheus-Metriken und -Logs am selben Ort visualisiert haben möchte. Wer mehr über Monitoring erfahren will, findet dazu einen Blogartikel.

Zudem läuft auf jedem unseren Kubernetes-Nodes der Logprozessor Fluent Bit, der alle Logs in der Standardeinstellung ungefiltert an OpenSearch schickt.

OpenSearch und Fluent Bit

Bei OpenSearch handelt es sich um einen Fork von Elasticsearch und Kibana. Die Anwendung OpenSearch ist eine Suchmaschine, die Ihre Daten im JSON-Format speichert. Zur Visualisierung verwendet man OpenSearch Dashboards.

Bei Fluent Bit handelt es sich um einen äußerst effizienten Logprozessor, der es uns ermöglicht, Logs aus den verschiedensten Quellen zu sammeln, aufzubereiten und an ein oder mehrere Ziele weiterzuleiten.

Beispiellogs

Nachfolgend betrachten wir die folgenden drei Logs:

  1. 2023/01/04 15:08:39 INFO respond with status 200 for /api/test after 200ms span_id:1234

  2. 2023/01/04 15:08:39 ERROR login attempt failed got 400 from /auth_v2/login?Authorization=Basic SnVzdDpEb250 call

  3. 2023/01/04 15:08:39 INFO add 5 values to cache

Das erste Log liefert wesentliche Informationen eines API-Calls. Da dieses Log nicht als JSON strukturiert ist, könnten wir in OpenSearch Dashboards zwar mit Suchfiltern arbeiten, jedoch keine sinnvollen Visualisierungen wie beispielsweise eine durchschnittliche Antwortzeit in einem Diagramm darstellen.
Beim zweiten Log werden Username und Passwort als Basic Auth herausgegeben. Jetzt stehen zunächst Username und Passwort eines Anwenders/einer Anwenderin im Applikationslog. Sollte man das Log nicht direkt ausbauen können, da einem die Applikation beispielsweise nicht gehört, würde dieses Log zusätzlich in OpenSearch landen und auf unbestimmte Zeit persistiert werden.
Das dritte Log könnte beispielsweise für Entwickler:innen relevant sein, die gerade ein neues Release einer Anwendung ausrollen und die korrekte Funktionsweise durch das Log überprüfen. Wir gehen also davon aus, dass nach jeder Operation n Werte in einem Cache gesichert werden und der Log allenfalls eine Daseinsberechtigung als Debug-Log hat und dieser aus Versehen als Info-Log in der finalen Anwendung auftaucht. Auch dieses Mal sind wir nicht Entwickler:innen der Applikation, wollen das Log jedoch bis zum Update nicht mehr in unserer Produktivumgebung sehen.

Logs filtern und verarbeiten

In der Standardkonfiguration von Fluent Bit in Kubernetes werden alle Containerlogs ungefiltert um Kubernetes-Metadaten erweitert und anschließend an die gewählte Datenbank weitergeschickt.

Wir werden nun unsere drei Logs auf drei verschiedene Arten in Fluent Bit bearbeiten, bevor diese an OpenSearch gesendet werden.

Disclaimer: Die Beispiele sind stark vereinfacht. Gehen Sie beim Erstellen von Regular Expressions behutsam vor, um keinen Regex Denial of Service zu generieren.

 

Log 1 – Timestamp: INFO respond with status 200 for /api/test after 200ms span_id:HASH

Hierzu benutzen wir einen Parser vom Typ Regex:

  1. [PARSER]
  2.     Name request_log
  3.     Format regex
  4.    Regex ^.+(?INFO).*status (?d+) for (?.+) after (?d+)ms span_id:(?.+)$

Den Parser rufen wir vorher über einen Filter auf, z.B.:

  1. [FILTER]
  2.   Name parser
  3.   Match *
  4.   Key_Name log
  5.    Parser request_log
  6.    Reserve_Data true

Wir haben soeben das Log mit unserem Parser strukturiert und können mit den Keys aus der Capture Group beispielsweise in OpenSearch Dashboards filtern oder mit den Werten Visualisierungen bauen. Durch die Span ID ist es uns zudem möglich, den API-Call in unserem Tracing-Tool zu finden und auszuwerten. Weitere Informationen zu Parsern finden Sie hier: https://docs.fluentbit.io/manual/pipeline/filters/parser

 

Visualisierung zum Zählen der erfolgreichen Antworten mit dem Status Code 200

Log 2 – Timestamp: ERROR login attempt failed got 400 from /auth_v2/login?Authorization=Basic SnVzdDpEb250 call

Wir könnten das Log mit Fluent Bit einfach verwerfen und nicht mit in die Datenbank aufnehmen. In diesem Fall gehen wir jedoch davon aus, dass wir generell die Informationen des Logs benötigen, jedoch nicht den Basic Auth String.
Hierzu bearbeiten wir den Logeintrag mithilfe von Lua:

  1. function remove_basic_auth(tag, timestamp, record)
  2.         if (record["log"] ~= nil)
  3.          then
  4.             tmp_log = record["log"]:gsub("(%Basic %S+)", "********")
  5.             record["log"] = tmp_log
  6.             return 1, timestamp, record
  7.         end
  8.             return 1, timestamp, record
  9.         end

Die Lua-Funktion rufen wir vorher über einen Filter auf, z.B.:

  1. [FILTER]
  2.    Name lua
  3.   Match *
  4.    script /fluent-bit/scripts/path_to_lua_script.lua
  5.  call remove_basic_auth

Wie man im Bild sehen kann, wurde der Basic Auth String durch Sterne ersetzt. Wir könnten jetzt beispielsweise noch mit dem Log arbeiten, ohne dass sich sensible Daten darin befinden. Ein Anwendungsfall könnte sein, dass ein Log nicht unbedingt ein Passwort enthält, jedoch eine bestimmte Information. Diese darf zwar im privaten Kubernetes Cluster gelesen werden, in der ausgelagerten Datenbank beim Cloud-Anbieter jedoch nicht.

Weitere Informationen zum Lua Filter gibt es hier: https://docs.fluentbit.io/manual/pipeline/filters/lua

Log 3 – Timestamp: INFO add 5 values to cache

Dieses Log brauchen wir nicht in unserer Datenbank und wollen es deshalb entfernen.
Dazu benötigen wir lediglich einen Grep-Filter, der einen Regex über die Logs laufen lässt und diese anhand der Einstellung verwirft oder behält. Das sieht bei diesem Log folgendermaßen aus:

[FILTER]
  Name grep
  Match *
  Exclude log ^.*add d+ values.*$

Ein beliebter Use Case von Grep ist das Entfernen von Logs aus bestimmten Applikationen. Wollen wir beispielsweise Logs aus einem bestimmten Namespace nicht haben, können wir die Logs mit Grep ignorieren. Weitere Informationen zum Grep Filter findet man hier: https://docs.fluentbit.io/manual/pipeline/filters/grep

Fazit

Mit guten Logs gewährleisten wir die Qualität unserer Systemlandschaft. Wir können jedoch nicht immer ausschließen, dass alle Logs, die unsere Applikationen erzeugen, einen Mehrwert liefern und diese auch einfach zu analysieren sind. Mit den richtigen Tools, beispielsweise mit Fluent Bit und OpenSearch, ist es uns jedoch möglich, notwendige Logs an einer zentralen Stelle zusammenzufassen und für unsere Anwendungsfälle zu strukturieren und visualisieren.

Kubernetes Training Partner

Kubernetes-Schulung

In nur drei Tagen lernen Teilnehmer:innen die Grundlagen von Kubernetes und dem Orchestrieren von Containern – von Aufbau und Verwaltung eines Container Cluster bis zu den wichtigsten Konzepten und Best Practices für den stabilen und sicheren Betrieb von Anwendungen im Kubernetes Cluster.

ATIX ist offizieller Kubernetes Training Provider

The following two tabs change content below.

Daniel Meng

Neueste Artikel von Daniel Meng (alle ansehen)