xPulse
🇩🇪 DE

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] ← @xpulse/controller
[router] ← @xpulse/router
[template] ← @xpulse/template
[http] ← @xpulse/http
[logger] ← @xpulse/logger selbst
[session] ← @xpulse/session / Feature
[presence] ← Feature im Client
[webrtc] ← Feature im Client
[app] ← @xpulse/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

| 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.

de/concept/message-structure.md 2026-03-30