Seiten
Inhalt
Logger – Log Message Structure Concept
Status: CONCEPT · Erstellt März 2026 Übergeordnet:
COMP_logger_spec.md,GLOBAL_concept-ecosystem.md
Warum ein eigenes Konzept?
Log-Ausgaben sind die wichtigste Debugging-Oberfläche im Betrieb. Inkonsistente Formate kosten Zeit – man sucht, filtert, grepped, und versteht trotzdem nicht sofort was passiert ist.
Dieses Konzept legt fest wie eine Log-Message aufgebaut ist, welche Felder verpflichtend sind, welche optional, und wie das Format je nach Kontext (Browser vs. Server, Development vs. Production) aussieht.
Anatomie einer Log-Message
Jede Log-Message besteht aus maximal fünf Teilen:
| [timestamp] (level) [module] message | key=value key=value |
| ─────────── ─────── ──────── ──────── ────────────────────── |
| 1 2 3 4 5 (optional) |
| # | Teil | Pflicht | Beschreibung |
|---|---|---|---|
| 1 | timestamp |
ja | Zeitstempel – Format je nach Kontext |
| 2 | level |
ja | Log-Level – immer 5 Zeichen breit (padding) |
| 3 | module |
ja | Woher kommt die Message – Package oder Feature |
| 4 | message |
ja | Die eigentliche Nachricht – kurz, präzise |
| 5 | context |
nein | Strukturierte Zusatzinfos als key=value Paare |
1. Timestamp
Server (Node.js)
ISO 8601, UTC, Millisekunden:
| [2026-03-09T10:23:45.123Z] |
Maschinenlesbar, sortierbar, Zeitzone eindeutig. Kein lokales Format.
Client (Browser)
Nur Zeit, keine Datum – das Browser-Fenster lebt nicht über Tage:
| [10:23:45.123] |
Kürzer, besser lesbar in der Browser Console.
2. Level
Immer 5 Zeichen breit – Padding mit Leerzeichen rechts. Das hält alle Zeilen untereinander ausgerichtet:
| (debug) |
| (info ) |
| (warn ) |
| (error) |
Vier Level, kein verbose, kein trace, kein fatal –
bewusst minimalistisch.
| Level | Bedeutung | Wann |
|---|---|---|
debug |
Entwicklerdetails | Normalfluss, nur in Development sichtbar |
info |
Relevante Ereignisse | Start, Stop, wichtige State-Änderungen |
warn |
Unerwartetes, aber kein Abbruch | Konfigurationsfehler, Fallbacks, Degradierung |
error |
Fehler der Aufmerksamkeit braucht | Ausnahmen, fehlgeschlagene Operationen |
Kein fatal – ein fataler Fehler wirft eine Exception und terminiert den Prozess.
Das muss nicht geloggt werden, das fällt auf.
3. Module
Der [module]-Teil identifiziert wo die Message herkommt –
nicht was passiert ist (das macht die Message selbst).
Konvention
| [package-name-ohne-xpulse-prefix] |
| [feature-slug] |
Beispiele:
| [controller] ← |
| [router] ← |
| [template] ← |
| [http] ← |
| [logger] ← selbst |
| [session] ← / Feature |
| [presence] ← Feature im Client |
| [webrtc] ← Feature im Client |
| [app] ← / Einstiegspunkt |
Breite
Kein festes Padding beim Module – Module-Namen sind unterschiedlich lang
und Padding würde bei langen Namen hässlich werden.
Stattdessen: eine konsistente Leerzeile nach dem schließenden ].
4. Message
Die eigentliche Nachricht. Lowercase, immer Englisch (Developer Layer – siehe GLOBAL_adr-013).
Regeln
Kurz und konkret. Eine Message beschreibt eine Aktion oder einen Zustand – kein Satz, keine Erklärung, kein Kontext (der kommt in den Context-Teil).
| ✓ route matched |
| ✓ controller discovered |
| ✓ navItem registered |
| ✓ connection timeout |
| ✗ Eine neue Route wurde erfolgreich gematchet und der Handler wurde aufgerufen |
Imperativ oder Partizip – keine Fragen, keine Ausrufe:
| ✓ server started |
| ✓ config loaded |
| ✓ request failed |
| ✗ server has been started! |
| ✗ could not load config? |
Keine Werte in der Message – Werte gehören in den Context-Teil:
| ✓ [controller] navItem registered | key=info_team href=/info/team |
| ✗ [controller] navItem info_team registered for /info/team |
Das macht grep auf die Message möglich, ohne über Werte zu stolpern.
5. Context (optional)
Strukturierte Zusatzinfos als key=value Paare, abgetrennt durch |.
| [2026-03-09T10:23:45.123Z] (info ) [http] request received | method=GET path=/info ip=192.168.1.0 |
Formatregeln
- Trenner:
|(Leerzeichen, Pipe, Leerzeichen) zwischen Message und Context-Block - Paare:
key=valueohne Anführungszeichen außer wenn der Wert Leerzeichen enthält - Reihenfolge: wichtigstes zuerst
- Kein Komma, kein JSON, kein Semikolon – nur Leerzeichen zwischen den Paaren
| | method=GET path=/info/team duration=12ms status=200 |
| | key=info_team href=/info/team label="Team Nav" |
| | controller=InfoController method=team path=/info/team |
Wann Context verwenden
Immer wenn ein Wert Teil des Ereignisses ist den man später filtern
oder suchen möchte. Faustregel: wenn du grep auf einen Wert anwendest,
gehört er in den Context.
Vollständige Beispiele
Server
| [2026-03-09T10:23:44.001Z] (info ) [app] server started | env=production port=3000 |
| [2026-03-09T10:23:44.012Z] (info ) [controller] ready | count=5 routes=8 |
| [2026-03-09T10:23:44.013Z] (info ) [controller] navigation ready | items=5 |
| [2026-03-09T10:23:45.001Z] (info ) [http] request received | method=GET path=/info ip=192.168.1.0 |
| [2026-03-09T10:23:45.008Z] (debug) [controller] controller called | controller=InfoController method=index |
| [2026-03-09T10:23:45.010Z] (debug) [template] render | view=info/index duration=2ms |
| [2026-03-09T10:23:45.012Z] (info ) [http] response sent | method=GET path=/info status=200 duration=11ms |
| [2026-03-09T10:23:46.001Z] (warn ) [controller] navItem duplicate key ignored | key=chat file=src/controllers/support/chat.js conflict=src/controllers/tool/chat.js |
| [2026-03-09T10:23:46.002Z] (error) [template] render failed | view=info/broken err=TemplateNotFound |
Client (Browser Console)
| [10:23:44.001] (info ) [session] login successful | user=alice |
| [10:23:44.120] (debug) [presence] peer connected | peerId=b3f7a |
| [10:23:44.300] (info ) [router] navigation | path=/peers |
| [10:23:48.001] (warn ) [webrtc] ice candidate missing | peerId=b3f7a |
| [10:23:55.120] (error) [session] decryption failed | key=chat_history |
Format je nach Kontext
| Server (Development) | Server (Production) | Client (Browser) | |
|---|---|---|---|
| Timestamp | [ISO 8601 UTC] |
[ISO 8601 UTC] |
[HH:MM:SS.mmm] |
| Level | (level) 5-stellig |
(level) 5-stellig |
(level) 5-stellig |
| Module | [module] |
[module] |
[module] |
| Message | Text | Text | Text |
| Context | key=value |
key=value |
key=value |
| Farbe | ja (ANSI) | nein | ja (%c CSS) |
| Ziel | Console + Datei | Console + Datei | Browser Console |
| Min-Level | debug |
info |
debug / off |
JSON-Format (optional, Production)
Für Log-Aggregatoren (z.B. Loki, Elasticsearch) kann der Server-Logger
alternativ als JSON ausgeben – konfigurierbar via xpulse.json:
| { |
| "log": { |
| "format": "json" |
| } |
| } |
Dann schreibt der Logger eine JSON-Zeile pro Eintrag (NDJSON):
| {"timestamp":"2026-03-09T10:23:45.123Z","level":"info","module":"http","message":"response sent","context":{"method":"GET","path":"/info","status":200,"duration":11}} |
Felder: timestamp, level, module, message, context (Objekt, nicht String).
Default ist immer human-readable Text – JSON nur wenn explizit konfiguriert.
Was `log.create(module)` zurückgibt
Der Module-Slug wird einmalig beim create() gesetzt und automatisch
in jede Message eingebaut:
| const log = logger.create('controller'); |
| log.debug('navItem registered', { key: 'info_team', href: '/info/team' }); |
| // → [2026-03-09T10:23:44.001Z] (debug) [controller] navItem registered | key=info_team href=/info/team |
| log.warn('navItem duplicate key ignored', { key: 'chat', file: '...', conflict: '...' }); |
| // → [2026-03-09T10:23:44.002Z] (warn ) [controller] navItem duplicate key ignored | key=chat file=... conflict=... |
Der zweite Parameter log.debug(message, context?) ist immer ein flaches Objekt –
keine verschachtelten Strukturen. Wer tief nested Daten loggen will,
serialisiert selbst und nutzt einen einzelnen data=... Key.
Abgrenzung zu `@xpulse/debug`
@xpulse/logger schreibt Log-Messages in Console und Datei –
es ist der Schreiber.
@xpulse/debug lauscht via logger:write Event auf jeden Log-Eintrag
und sammelt ihn für den Web Profiler – es ist der Leser.
Das Format bleibt identisch. @xpulse/debug parst keine Log-Messages –
es bekommt die strukturierten Daten direkt über das Event:
| event.emit('logger:write', { |
| level: 'debug', |
| module: 'controller', |
| message: 'navItem registered', |
| context: { key: 'info_team', href: '/info/team' }, |
| timestamp: new Date().toISOString(), |
| }); |
Das bedeutet: das Logger-Konzept bleibt sauber getrennt von der Debug-Visualisierung.