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:
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)
------------------------------------------------