Logging
Structured logs, scoped channels & request tracing
Nucleus ships with a structured logger that every internal subsystem writes through. The logging block tunes severity (level), which feature areas speak (scopes), how output is rendered (prettyPrint / colorize / includeCallerInfo), what gets redacted (redactKeys), and per-request access logging with end-to-end tracing (requests).
Severity filtering is the familiar threshold model. Scope filtering is the powerful part — because each log line is tagged with a dotted scope like authentication.login or entity.create, you can silence entire subsystems or zoom in on exactly one while debugging, without touching code. On top of that, every request gets a server-generated x-request-id that appears in the access log, error log and authorization decisions — grep one id and you have the request's whole story.
Severity level#
A single threshold. Any message below the configured level is dropped before it reaches a transport. Levels are ordered debug < info < warn < error < fatal, so level: "warn" keeps warnings, errors and fatals while discarding debug and info chatter.
level'debug' | 'info' | 'warn' | 'error' | 'fatal'OptionalThe minimum severity that will be emitted. The root mode option drives the defaults: with mode: 'development' the level defaults to debug and output is pretty-printed, colorized and carries caller file:line; otherwise it defaults to info with compact single-line JSON.
debug— Everything, including verbose internal tracing. Ideal locally, far too noisy for production.info— Lifecycle and notable events. The default and a sensible production baseline.warn— Recoverable problems and suspicious conditions only.error— Failed operations only.fatal— Process-threatening failures only.
"info" ("debug" when mode: "development")Scope filtering#
Scopes are dotted identifiers — category.subcategory — attached to log lines across the framework. The scopes array is an allow-list: only messages whose scope matches an entry are shown. This is orthogonal to level, so you combine them (e.g. only warnings, and only from authentication).
1{2 "logging": {3 "level": "debug",4 "scopes": ["authentication.*", "middleware.auth"]5 }6}scopesstring[]OptionalAllow-list of scopes to emit. Use "*" to enable everything (the default). Use a category wildcard like "authentication.*" to keep one subsystem. Provide exact scopes like "tenant.provision" for surgical focus. An empty array [] silences all scoped logs, leaving only unscoped framework messages.
Example: ["authentication.*", "tenant.provision", "entity.create"]
["*"]Output formatting & redaction#
How emitted lines look and what never reaches them. All four default from the root mode option — set them explicitly to override.
prettyPrintbooleanOptionalMulti-line, human-readable output. When false, each entry is a single JSON line — the right choice for log aggregators.
true in development modecolorizebooleanOptionalANSI colors per level in console output.
true in development modeincludeCallerInfobooleanOptionalAttach the real call site (file:line, skipping logger-internal frames) to every entry. Costs a stack capture per line — leave it off in production.
true in development moderedactKeysstring[]OptionalExtra context keys to mask as [REDACTED]. These EXTEND the built-in list (password, secret, token, apiKey, authorization, cookie, …) — the built-ins always apply. Matching is case-insensitive and recursive through nested context objects.
Example: ["customerTaxId", "iban"]
[]Request logging & tracing#
Every request is assigned a server-generated x-request-id (inbound values are stripped, so ids can't be spoofed), echoed back as a response header and attached to the access log, error log and authorization-decision logs. The requests block controls the access log itself.
1{2 "logging": {3 "level": "info",4 "prettyPrint": false,5 "redactKeys": ["iban"],6 "requests": {7 "slowThresholdMs": 1000,8 "excludePaths": ["/health", "/metrics*"]9 }10 }11}requests.enabledbooleanOptionalOne completion line per request — ← METHOD /path STATUS (Nms) — with requestId, userId, authType, tenant, ip and durationMs, under scope middleware.request. Levels auto-select: 2xx/3xx → info, 4xx or slow → warn, 5xx → error.
truerequests.logArrivalbooleanOptionalAlso emit a debug '→ METHOD /path' line when the request arrives — useful for spotting requests that never complete.
falserequests.includeQuerybooleanOptionalInclude the query string in the logged path.
truerequests.slowThresholdMsnumberOptionalRequests slower than this log at warn with slow: true, regardless of status.
3000requests.excludePathsstring[]OptionalPaths to skip — exact matches or trailing-* prefixes like "/metrics*". Keeps probes and scrapers out of your logs.
["/health"]Under the hood — the Logger#
A single Logger instance (exported as logger, also Logger.getInstance()) backs every subsystem. It is a structured logger with pluggable transports, request correlation and built-in audit — the two config dials above are the tip of it.
scoped(scope)ScopedLoggerOptionalHow the scope tags above are produced: a subsystem calls logger.scoped('authentication.login') and every line it emits carries that scope. If the scope isn't in your allow-list the calls become genuine no-ops — filtering happens at the source, not just at output, so disabled scopes cost almost nothing.
child(context) / withCorrelationId(id)LoggerOptionalPer-request child loggers carry a correlation id (shown as the first 8 chars in pretty output) and a merged context object, so every line from one request is traceable. Context is passed through redactSensitiveData first, so configured secret keys never reach a transport.
transportsConsoleTransport · BufferedTransport · DatabaseAuditTransportOptionalConsole output follows prettyPrint/colorize/includeCallerInfo (defaulted from mode, overridable per key). BufferedTransport batches entries (flush at 100 or every 1s). Audit is part of the logger: a DatabaseAuditTransport persists audit entries to your audit table — addTransport/addAuditTransport let you attach more sinks.
request() / db() / time() / timeAsync()helpersOptionalConvenience emitters used across the framework: request() logs method/path/status and auto-selects level by status (5xx→error, 4xx→warn, else info); db() logs a query with table + duration + row count; time()/timeAsync() measure and log an operation's duration.
audit() / trace()audit bridgeOptionalThe same logger writes the immutable audit trail (see the Audit section). trace() can both log and conditionally write an audit entry in one call, gated by auditEnabled — which is why audit and logging share this one component.
Related sections