Zum Inhalt

Logging-Kontext & Single-Line JSON-Logging

Die Microservices der Open Data Infrastruktur (ODI) nutzen ein zentralisiertes, strukturiertes Logging auf Basis von Pino / NestJS-Pino. Auf allen produktiven und stagingnahen Umgebungen ist das JSON-Logging aktiviert (LOG_JSON=true).


Konzept: Single-Line JSON-Logging

In verteilten Systemen zerschneiden traditionelle mehrzeilige Logs (z. B. Stacktraces über 20 Zeilen) das Log-Stream-Aggregat (Loki/Promtail), was zu unvollständigen Log-Fragmenten und erschwerter Fehlersuche führt.

In den ODI-Diensten gilt daher die strikte Regel: Jedes Fehler-Event wird als genau ein durchsuchbares JSON-Objekt auf einer einzigen Zeile ausgegeben.

Vorteile von Single-Line JSON:

  • Ein Log-Eintrag = Ein Fehler-Event: Kein Zerschneiden von Stacktraces in Loki/Grafana.
  • Vollständigkeit: Stacktraces (error.stack), Anfragedaten (request) und verschachtelte Fehlerursachen (internalError) bleiben vollständig im Objekt erhalten.
  • Automatische Auswertbarkeit: Logql-Queries in Grafana können jedes Feld direkt indizieren und filtern.

Aufbau des JSON-Log-Objekts

Ein Fehler-Log-Eintrag besitzt folgende standardisierte Struktur:

{
  "level": 50,
  "time": "27.07.2026, 13:05:00",
  "context": "CkanClient",
  "requestId": "b92f180a-912c-4c6e-821f-0e1234567890",
  "timestamp": "27.07.2026, 13:05:00",
  "request": {
    "path": "/backend/datasets/sync",
    "method": "POST",
    "requestId": "b92f180a-912c-4c6e-821f-0e1234567890"
  },
  "error": {
    "name": "ServerError",
    "message": "Connection failed to Ckan",
    "context": "CkanClient",
    "stack": "Error: Connection failed to Ckan\n    at CkanClient.post (/app/dist/ckan.client.js:42:11)\n    at processTicksAndRejections (node:internal/process/task_queues:95:5)",
    "type": "INTERNAL"
  },
  "msg": "[ServerError] Connection failed to Ckan | at CkanClient.post (/app/dist/ckan.client.js:42:11)"
}

Erklärung der Log-Felder

1. context (Dynamischer Log-Kontext)

Anstatt des generischen Filter-Namens (z. B. AllExceptionsFilter) wird im Feld context dynamisch der tatsächliche Ursprung im Code ermittelt: * Befindet sich am Fehler-Objekt ein context-Attribut (z. B. bei custom ServerError oder ApiError), wird dieses verwendet (z. B. "CkanClient", "UserService", "MetadataService"). * Andernfalls wird der Fehler-Klassenname genommen (z. B. "TypeError", "AxiosError").

Vorteil für den Admin: In der Log-Spalte context ist sofort ersichtlich, welches Modul oder welche Komponente den Fehler verursacht hat.

2. msg (Formatiertes Log-Message-Signal)

Die Nachricht msg wird dynamisch im Format [FehlerName] Fehlermeldung | at CodeOrt aufgebaut: * Beispiel: [ServerError] Connection failed to Ckan | at CkanClient.post (/app/dist/ckan.client.js:42:11) * Vorteil für den Admin: In Grafana Loki sieht man direkt in der Log-Zeilenvorschau die konkrete Ursache und den Aufrufort, ohne den JSON-Baum aufklappen zu müssen.

3. requestId (Correlation ID)

Die eindeutige Request ID der eingehenden HTTP-Anfrage (siehe Fehleranalyse & Request ID). Über diese ID kann der gesamte Anfrageverlauf serviceübergreifend im Grafana Dashboard gefiltert werden: * 🚀 Produktion (Prod): grafana.odi.schleswig-holstein.de/d/betrieb-request-id-v1/request-id-suche * 🧪 Staging (Stage): grafana.odi-stage.schleswig-holstein.de/d/betrieb-request-id-v1/request-id-suche

4. error & Verschachtelte Fehler (internalError)

Das error-Objekt wird von der Filter-Pipeline serialisiert. Liegt ein verschachtelter Fehler vor (z. B. ein ApiError, der einen ServerError kapselt, welcher wiederum einen ClientError mit HTTP 502 Daten enthält), wird die gesamte Fehlerkette unter internalError lückenlos im JSON dargestellt.


Dienste ohne JSON-Logging (Text-Logs)

Nicht alle Dienste können JSON ausgeben (z. B. Keycloak, GeoServer, nginx-Frontends, Python-Dienste). Damit Level-Filter und Fehler-Panels auch dort greifen, ergänzt Promtail beim Einsammeln das Level – die Logzeile selbst bleibt unverändert. Stage und Produktion verwenden dieselbe Konfiguration (clusters/<env>/grafana-loki/promtail/promtail.configmap.yaml).

Format Dienste Von Promtail ergänzt
Keycloak (Quarkus) keycloak Level, context (Logger), Event-Felder, Stacktrace als ein Eintrag
X-Road (Java) xroad-security-server (nur Stage) Level, context (Teildienst), Stacktrace als ein Eintrag
GeoServer geoserver Level, context (Logger), Folgezeilen als ein Eintrag
Python logging triple-converter-worker Level, context (Modul)
Fuseki apache-jena-fuseki Level, context
uvicorn frost-proxy, frictionless-service Level
frictionless-Applog frictionless-service Level, request_id
winston udp-manager, visualization-backend Level
nginx-Error-Log Frontends Level
nginx-Access-Log Frontends Level aus dem Statuscode: 5xx = error, 4xx = warn, sonst info
Redis redis Level aus dem Symbol (# = warn, * = info)

Die Level-Werte sind überall dieselben wie bei den JSON-Diensten: trace, debug, info, warn, error, fatal.

Zeilen ohne Level

Ausgaben ohne Level-Angabe bleiben ohne Level, z. B. Startmeldungen der nginx-Entrypoint-Skripte. Die Infrastruktur-Namespaces (kube-system, Loki, Flux, Envoy) sind nicht Teil dieses Mappings.

Keycloak: Login- und Token-Events

Keycloak läuft im eigenen Namespace keycloak. Aus den Event-Zeilen des Loggers org.keycloak.events zieht Promtail vier Felder als Structured Metadata:

Feld Inhalt Beispiel
kc_event Event-Typ LOGIN_ERROR, REFRESH_TOKEN_ERROR
kc_client Client, über den der Login lief odw, schema-repository-frontend
kc_realm Realm open-data-infrastruktur
kc_error Fehlergrund invalid_user_credentials, invalid_token

Beispiel-Abfragen für Grafana Explore:

{namespace="keycloak"} | kc_event=~".*LOGIN_ERROR"
sum by (kc_client, kc_error) (count_over_time({namespace="keycloak"} | kc_event!="" [24h]))

Nur fehlgeschlagene Logins

Keycloak schreibt in der Standardeinstellung nur Fehler-Events ins Log (Level warn). Erfolgreiche Logins erscheinen nicht.


Verhalten bei lokaler Entwicklung (LOG_JSON=false)

Bei der lokalen Entwicklung im Terminal ist LOG_JSON standardmäßig deaktiviert (false). Für maximale Lesbarkeit beim Entwickeln wird dort weiterhin eine formatierte, mehrzeilige Konsolenausgabe mit Trennlinien erzeugt:

------------------------------------------------
| ERROR DETAILS |
------------------------------------------------
Timestamp: 27.07.2026, 13:05:00
Request ID: b92f180a-912c-4c6e-821f-0e1234567890
Request Path: /backend/datasets/sync
Method: POST
Error Message: Connection failed to Ckan
Environment Mode: DEVELOPMENT

Stack Trace:
------------------------------------------------
Error: Connection failed to Ckan
    at CkanClient.post (/app/dist/ckan.client.js:42:11)
------------------------------------------------