Log the event,
not just the text.
Write one message template and logit captures a structured event from it: the template, every named value, the ambient request context, the error with its cause chain. Every sink — console, file, Seq, OTLP, your own — sees the same data.
MIT · zero dependencies · ESM + CJS · TypeScript
log.info('Order {OrderId} shipped to {@Address} in {Elapsed:0.0} ms',
1042, { city: 'Budapest', zip: '1011' }, 83.2);08:12:03.123 INF [shop.orders] Order 1042 shipped to { city: "Budapest", zip: "1011" } in 83.2 ms RequestId=req_9f3aFour ideas, borrowed from the library that got .NET logging right.
- 01
Write a template
log.info('User {UserId} bought {@Order}', 42, order). The holes are named, so the values keep their names.@captures an object's structure;$forces a string. - 02
Capture, don’t format
The message is never rendered at the call site. The event stores the template and the captured values — depth-limited, cycle-safe, redacted by key — and each sink renders them its own way, or not at all.
- 03
Enrich from context
Run a request inside
LogContextand every event written in it carries the request id, however deep the call stack and across everyawait. Enrichers add the host, the environment, the trace. - 04
Ship to any sink
Pretty on a terminal, JSON in a container, CLEF to Seq, OTLP to a collector, a rolling file, a batched HTTP endpoint — each with its own minimum level, each flushed on
close().
What Serilog taught .NET, for the JavaScript runtimes.
Message templates
Named holes, positional holes, :format and ,alignment — the messagetemplates.org grammar Serilog uses, so a template is a queryable event type, not a string to regex later.
Structured capture
Plain objects are data and come through whole; class instances need @; $ stringifies. Depth, string and collection limits, cycle safety, transforming policies and key-based redaction all apply once, at capture.
Levels, overrides, switches
Six levels. A minimum per source prefix — next at warn, app.db at verbose — and a LevelSwitch you flip at runtime from an admin route or a signal.
Ambient context and enrichers
LogContext.run({ requestId }, …) rides on AsyncLocalStorage, so the id reaches every event under it. Enrichers add the host, the process, the environment, the trace.
Sinks, and loggers as sinks
Console, rolling file, batched HTTP, Seq, OTLP, memory — each with its own minimum level. A logger is a sink too, so sub-loggers, conditional and audit sinks come for free.
DocsOutput templates, JSON, CLEF, pretty
{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception} works as written. Flat JSON for log pipelines, CLEF with the @i event-type hash for Seq, colour for the terminal.
Errors done properly
Pass the error first and the event carries its name, message, stack, own properties, the cause chain and an AggregateError’s members — rendered by every formatter, structured in every sink.
Request logging with a diagnostic context
One completion event per request for Next.js route handlers, middleware and onRequestError, and for Express. Set a value anywhere in the request and it lands on that event.
Timed operations
log.timed('Sync {Tenant}', fn) completes or abandons with Elapsed and Outcome. Or begin one, enrich it as you go, and let using abandon it if you forget.
Node, Bun, edge, browser. Zero dependencies.
The root entry has no Node imports and ships ESM and CJS with types. Node extras, Next.js, Express and OpenTelemetry are subpaths you import only where they apply.
DocsWhere it writes today, and what comes next.
Out of the box logit writes to the console, rolling files, any HTTP endpoint, Seq and OpenTelemetry collectors, and hooks into Next.js and Express. The rest of Serilog’s ecosystem is a roadmap, not a promise — here is the order we’re working in.
Console sink
Available todayPretty on a terminal, JSON on a pipe, CLEF or a text output template on request; stderr from a level; the browser console with real objects.
DocsRolling file sink
Available todaySynchronous appends (nothing lost on a crash), rolling by minute / hour / day / month and by size, a retained-file count.
DocsHTTP sink
Available todayBatches with a size and an interval, retries with backoff, a bounded queue, NDJSON / array / CLEF bodies,
Docspagehideflush in browsers.Seq
Available todayCLEF over HTTP to
Docs/ingest/clefwith the API key header — Seq’s native format,@ievent types and all.OpenTelemetry
Available todayOTLP/HTTP JSON to any collector without the SDK, honouring
DocsOTEL_EXPORTER_OTLP_*; or a bridge sink that hands events to an SDKLoggerProvider.Request logging for Next.js and Express
Available todayOne completion event per request with method, path, status and elapsed time; a diagnostic context;
DocswithLogging()for route handlers and middleware,createOnRequestError()for instrumentation,requestLogger()for Express.Expression language
Up nextFilters, conditional sinks and
ExpressionTemplateformatting written as text —RequestPath like '/health%'— instead of predicates in code.Configuration from JSON and env
Up nextA whole logger from a JSON document or environment variables, with a registry that maps sink names to sinks.
Durable HTTP and Seq shipping
Up nextA disk buffer in front of the network, so events written while the collector is down are sent after a restart.
Remote level control
Up nextSeq (or any endpoint) tells the logger which minimum level to run at, and a
LevelSwitchfollows.Automatic trace context
Up nextTrace and span ids read from the active OpenTelemetry span on every event, with no getter to write.
Vendor sink packages
PlannedDatadog, Grafana Loki, Splunk HEC, Elasticsearch / ECS, CloudWatch, Application Insights, Sentry.
Worker-thread transports
PlannedHeavy sinks run off the event loop, the way pino transports do.
Pattern-based masking
PlannedEmails, IBANs, card numbers and your own patterns masked inside any string, not only by key.
Fastify, Hono, Koa, NestJS
PlannedRequest logging and a
LoggerServicefor the other servers.