xPulse
πŸ‡¬πŸ‡§ EN

Logger – Log Message Structure Concept

Status: CONCEPT Β· Created March 2026 Parent: COMP_logger_spec.md, GLOBAL_concept-ecosystem.md


Why a dedicated concept?

Log output is the most important debugging surface in operation. Inconsistent formats cost time β€” you search, filter, grep, and still don't immediately understand what happened.

This concept defines how a log message is structured, which fields are mandatory, which are optional, and how the format looks depending on context (browser vs. server, development vs. production).


Anatomy of a Log Message

Every log message consists of at most five parts:

[timestamp] (level) [module] message | key=value key=value
─────────── ─────── ──────── ──────── ──────────────────────
1 2 3 4 5 (optional)
# Part Required Description
1 timestamp yes Timestamp – format depends on context
2 level yes Log level – always 5 characters wide (padded)
3 module yes Where the message comes from – package or feature
4 message yes The actual message – short, precise
5 context no Structured additional info as key=value pairs

1. Timestamp

Server (Node.js)

ISO 8601, UTC, milliseconds:

[2026-03-09T10:23:45.123Z]

Machine-readable, sortable, timezone unambiguous. No local format.

Client (Browser)

Time only, no date β€” a browser window doesn't live for days:

[10:23:45.123]

Shorter, more readable in the browser console.


2. Level

Always 5 characters wide β€” padded with spaces on the right. This keeps all lines aligned:

(debug)
(info )
(warn )
(error)

Four levels, no verbose, no trace, no fatal β€” deliberately minimalist.

Level Meaning When
debug Developer details Normal flow, only visible in development
info Relevant events Start, stop, important state changes
warn Unexpected but no abort Configuration errors, fallbacks, degradation
error Error that needs attention Exceptions, failed operations

No fatal β€” a fatal error throws an exception and terminates the process. That doesn't need to be logged, it's immediately obvious.


3. Module

The [module] part identifies where the message comes from β€” not what happened (the message itself handles that).

Convention

[package-name-without-xpulse-prefix]
[feature-slug]

Examples:

[controller] ← @xpulse/controller
[router] ← @xpulse/router
[template] ← @xpulse/template
[http] ← @xpulse/http
[logger] ← @xpulse/logger itself
[session] ← @xpulse/session / feature
[presence] ← feature in client
[webrtc] ← feature in client
[app] ← @xpulse/app / entry point

Width

No fixed padding for the module β€” module names vary in length and padding would look ugly for long names. Instead: a consistent space after the closing ].


4. Message

The actual message. Lowercase, always English (Developer Layer β€” see GLOBAL_adr-013).

Rules

Short and concrete. A message describes an action or state β€” not a sentence, not an explanation, not context (that goes in the context part).

βœ“ route matched
βœ“ controller discovered
βœ“ navItem registered
βœ“ connection timeout
βœ— A new route was successfully matched and the handler was called

Imperative or past participle β€” no questions, no exclamations:

βœ“ server started
βœ“ config loaded
βœ“ request failed
βœ— server has been started!
βœ— could not load config?

No values in the message β€” values belong in the context part:

βœ“ [controller] navItem registered | key=info_team href=/info/team
βœ— [controller] navItem info_team registered for /info/team

This makes grep on the message possible without stumbling over values.


5. Context (optional)

Structured additional info as key=value pairs, separated by |.

[2026-03-09T10:23:45.123Z] (info ) [http] request received | method=GET path=/info ip=192.168.1.0

Format Rules

| 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

When to Use Context

Whenever a value is part of the event that you might want to filter or search for later. Rule of thumb: if you would grep for a value, it belongs in the context.


Full Examples

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 by Context

Server (Development) Server (Production) Client (Browser)
Timestamp [ISO 8601 UTC] [ISO 8601 UTC] [HH:MM:SS.mmm]
Level (level) 5-char (level) 5-char (level) 5-char
Module [module] [module] [module]
Message text text text
Context key=value key=value key=value
Colour yes (ANSI) no yes (%c CSS)
Target console + file console + file browser console
Min level debug info debug / off

JSON Format (optional, production)

For log aggregators (e.g. Loki, Elasticsearch) the server logger can optionally output JSON β€” configurable via xpulse.json:

{
"log": {
"format": "json"
}
}

The logger then writes one JSON line per entry (NDJSON):

{"timestamp":"2026-03-09T10:23:45.123Z","level":"info","module":"http","message":"response sent","context":{"method":"GET","path":"/info","status":200,"duration":11}}

Fields: timestamp, level, module, message, context (object, not string).

Default is always human-readable text β€” JSON only when explicitly configured.


What `log.create(module)` Returns

The module slug is set once during create() and automatically included in every message:

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

The second parameter log.debug(message, context?) is always a flat object β€” no nested structures. Anyone needing to log deeply nested data serialises it themselves and uses a single data=... key.


Separation from `@xpulse/debug`

@xpulse/logger writes log messages to console and file β€” it is the writer.

@xpulse/debug listens via the logger:write event to every log entry and collects it for the Web Profiler β€” it is the reader.

The format is identical. @xpulse/debug does not parse log messages β€” it receives the structured data directly via the event:

event.emit('logger:write', {
level: 'debug',
module: 'controller',
message: 'navItem registered',
context: { key: 'info_team', href: '/info/team' },
timestamp: new Date().toISOString(),
});

This means: the logger concept remains cleanly separated from the debug visualisation.

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