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}.
| Token | Renders |
|---|---|
{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 name | that 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:
| Pattern | Output |
|---|---|
yyyy yy | 2026 · 26 |
MM M | 10 · 10 |
dd d | 08 · 8 |
HH H hh h | 08 · 8 · 08 · 8 (12-hour) |
mm ss | minutes, seconds |
fff ff f FFF | milliseconds; F drops trailing zeros |
tt | AM / PM |
zzz zz z | +02:00 · +02 · +2 |
K | Z or the offset |
o O | ISO 8601 (toISOString()) |
'T' "T" \T | a 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: 402Time, 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.
| Option | Default |
|---|---|
colors | false — the console sink passes its detection |
timestamp | 'HH:mm:ss.fff'; false hides it |
utc | false |
extras | true — show key=value for properties outside the message |
source | true — 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"}| Field | From |
|---|---|
timestamp | ISO 8601, or epoch ms with timestampFormat: 'epoch' |
level | info; levelFormat: 'number' → 30, 'serilog' → Information, 'upper' → INFO |
message | the rendered message (message: false to omit) |
template | the template text (template: false to omit) |
source | SourceContext, when set |
traceId, spanId | when set |
eventId | the template hash, with eventId: true |
error | the error as a structure: name, message, stack, own properties, cause, errors |
| everything else | the 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-ishCollisions. 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"}| Field | Meaning |
|---|---|
@t | timestamp, ISO 8601 |
@mt | the message template |
@m | the rendered message — only with renderMessage: true (Serilog's "rendered compact" variant) |
@l | Serilog's level name; omitted for info, as Serilog does |
@x | the error as text: stack and cause chain |
@i | the event type: the template's hash, the same value Serilog computes |
@r | the rendered text of every hole that has a format specifier, in order |
@tr, @sp | trace and span ids |
| everything else | the 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