Concepts

Formatting

Output templates, the JSON and CLEF formatters, the pretty console.

A formatter turns an event into text: (event: LogEvent) => string, no trailing newline. The console, stream, file and HTTP sinks take one as format, by name or as a function. Four are built in.

Output templates — textFormatter#

Serilog's text formatter. The output template is itself a message template whose holes are the event's fields:

import { textFormatter, consoleSink } from '@unhingged/logit';

consoleSink({ format: 'text', outputTemplate: '{Timestamp:HH:mm:ss.fff} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}' });
// or
textFormatter({ outputTemplate: '…', utc: true });

The default is Serilog's: {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}.

TokenRenders
{Timestamp}ISO 8601; with a format, the date pattern below. Local time unless utc: true
{Level}Information; :u3 → INF, :w3 → inf, :u → INFORMATION, :w → information, :u1 → I
{Message}the rendered message; :l literal strings, :j JSON structures — :lj is the usual
{NewLine}\n
{Exception}the error's stack, then each cause indented, an AggregateError's members numbered; empty without an error
{Properties}every property not used by the message or by the output template, as { Key: value }; :j for JSON
{SourceContext}, {RequestId}, any namethat property, rendered literally; empty when absent
{TraceId}, {SpanId}the trace fields
{EventId}the template hash

Alignment works on every token: [{Level:u3}] {SourceContext,-20}.

Date patterns#

The .NET patterns, so a Serilog template is a logit template:

PatternOutput
yyyy yy2026 · 26
MM M10 · 10
dd d08 · 8
HH H hh h08 · 8 · 08 · 8 (12-hour)
mm ssminutes, seconds
fff ff f FFFmilliseconds; F drops trailing zeros
ttAM / PM
zzz zz z+02:00 · +02 · +2
KZ or the offset
o OISO 8601 (toISOString())
'T' "T" \Ta literal

Number formats#

On any numeric property, in a message or an output template: 0.00, 0.#, 000, F2, N0, P1, D3, X8. See Message templates.

Pretty — prettyFormatter#

The development console:

08:12:03.123 INF [shop.orders] Order 1042 shipped to { city: "Budapest", zip: "1011" } in 83.2 ms RequestId=req_9f3a
08:12:03.140 ERR [shop.payments] Charge for 1042 failed
    Error: card declined
        at charge (…/payments.ts:42:11)
      caused by: GatewayError: 402

Time, a coloured three-letter level, the source in brackets, the message with its values coloured (strings cyan, numbers magenta, punctuation dim), the properties not in the message as dimmed key=value, and the error block indented.

OptionDefault
colorsfalse — the console sink passes its detection
timestamp'HH:mm:ss.fff'; false hides it
utcfalse
extrastrue — show key=value for properties outside the message
sourcetrue — show [SourceContext]

detectColors(stream) is the console sink's rule, exported: colours when the stream is a TTY, never with NO_COLOR set, always with FORCE_COLOR set (other than 0), never with TERM=dumb.

JSON — jsonFormatter#

One flat object per event: the reified fields first, then every property at the root — the shape log pipelines index best.

{"timestamp":"2026-10-08T08:12:03.123Z","level":"info","message":"Order 1042 shipped to { city: \"Budapest\", zip: \"1011\" } in 83.2 ms","template":"Order {OrderId} shipped to {@Address} in {Elapsed:0.0} ms","source":"shop.orders","OrderId":1042,"Address":{"city":"Budapest","zip":"1011"},"Elapsed":83.2,"RequestId":"req_9f3a"}
FieldFrom
timestampISO 8601, or epoch ms with timestampFormat: 'epoch'
levelinfo; levelFormat: 'number' → 30, 'serilog' → Information, 'upper' → INFO
messagethe rendered message (message: false to omit)
templatethe template text (template: false to omit)
sourceSourceContext, when set
traceId, spanIdwhen set
eventIdthe template hash, with eventId: true
errorthe error as a structure: name, message, stack, own properties, cause, errors
everything elsethe properties

Rename the reified keys for a pipeline that expects pino's or ECS's names:

jsonFormatter({ keys: { timestamp: 'time', message: 'msg' }, levelFormat: 'number' }); // pino-shaped
jsonFormatter({ keys: { timestamp: '@timestamp', level: 'log.level' } });             // ECS-ish

Collisions. A property named like a reified key — a level of your own — is written as level_, so nothing is lost silently. Non-finite numbers become strings; bigint becomes its digits.

CLEF — clefFormatter#

The Compact Log Event Format, Serilog's own wire format and what Seq ingests natively:

{"@t":"2026-10-08T08:12:03.123Z","@mt":"Order {OrderId} shipped to {@Address} in {Elapsed:0.0} ms","@i":"d582b83a","@r":["83.2"],"OrderId":1042,"Address":{"city":"Budapest","zip":"1011"},"Elapsed":83.2,"SourceContext":"shop.orders","RequestId":"req_9f3a"}
FieldMeaning
@ttimestamp, ISO 8601
@mtthe message template
@mthe rendered message — only with renderMessage: true (Serilog's "rendered compact" variant)
@lSerilog's level name; omitted for info, as Serilog does
@xthe error as text: stack and cause chain
@ithe event type: the template's hash, the same value Serilog computes
@rthe rendered text of every hole that has a format specifier, in order
@tr, @sptrace and span ids
everything elsethe properties; a name starting with @ is escaped as @@

Media type: application/vnd.serilog.clef (CLEF_MEDIA_TYPE).

Picking one by name#

resolveFormatter('json' | 'clef' | 'text' | 'pretty' | fn, { outputTemplate, colors, utc }) is what the sinks call; use it in a sink of your own.

Rendering by hand#

import { renderValue, renderTemplate, formatDate, formatNumber, formatLevel, formatError, toJson } from '@unhingged/logit';

event.renderMessage({ literal: true });          // the message, strings unquoted
renderValue({ a: 1 }, 'j');                      // '{"a":1}'
formatDate(new Date(), 'yyyy-MM-dd HH:mm');
formatLevel('warn', 'u3');                       // 'WRN'
formatError(err);                                // stack + causes