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.*", "authorization.check"]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. prettyPrint, colorize and includeCallerInfo default from the root mode option; redactKeys defaults to [] and always extends the built-in sensitive-key list. Set any 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 a case-insensitive SUBSTRING test (not whole-key), recursive through nested context objects — a key is redacted if it CONTAINS any configured or built-in term. So the built-in "token" already masks refreshToken and emailVerificationToken; avoid short terms like "id" that would over-match userId, requestId or tenantId. The runtime redact list is also extended at boot by any config.entities column declared sensitive: true — registered in both its snake_case and camelCase spellings — so marking a column sensitive redacts it everywhere the logger redacts. Redaction applies not only to log context but also to the old/new value diffs Logger.audit writes to the audit_logs table: a key you add here (or a column you mark sensitive) is masked in both the logs and the immutable audit trail, so PII never lands as cleartext in the durable copy.
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 that reaches a route handler — ← 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. The ← line is written in onAfterHandle, so a request short-circuited earlier in onRequest by returning a Response (rather than reaching a handler) produces no completion line.
truerequests.logArrivalbooleanOptionalAlso emit a debug '→ METHOD /path' line when the request arrives — useful for spotting requests that never complete.
falserequests.includeQuerybooleanOptionalInclude the raw query string as a separate `query` field in the request log entry — the message path itself stays query-free (so `← GET /foo 200` stays clean while `query: "?bar=1"` lives in the entry context).
truerequests.slowThresholdMsnumberOptionalRequests slower than this are tagged slow: true and log at warn — unless the status is 5xx, which still logs at error (status severity takes precedence).
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 available on the Logger for structured request/query/timing entries: 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. (The framework's own per-request access log is emitted via logger.log(...) under the `middleware.request` scope, not via request().)
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