ReferenceObservability
@nextrush/logger

Logger

Request logging middleware with correlation IDs and structured output.

HTTP requests leave no trace by default. When a production issue hits, you need structured logs with correlation IDs to track requests across services and find the failure point.

The logger middleware solves this: structured request/response logging, automatic correlation ID tracking, and a request-scoped logger attached to every context as ctx.log.

Source & internals


Default Behavior

With default options, logger():

  • Logs request completion with method, path, status code, and duration
  • Generates a UUID correlation ID per request (via crypto.randomUUID())
  • Sets the correlation ID in the x-request-id response header
  • Attaches a scoped logger to ctx.log with the correlation ID
  • Logs request start in development only (based on isProductionBuild())
  • Uses info level for 2xx/3xx, warn for 4xx, error for 5xx responses

Installation

$ pnpm add @nextrush/logger

This package wraps @nextrush/log and re-exports all its functionality.


Minimal Usage

Register the logger middleware before route handlers. It must run first to attach ctx.log and track request timing.

import { createApp } from '@nextrush/core';
import { logger } from '@nextrush/logger';

const app = createApp();

app.use(logger());

app.use(async (ctx) => {
  ctx.log.info('Processing request');
  ctx.json({ ok: true });
});

The middleware attaches ctx.log — a request-scoped logger that includes the correlation ID in every log entry.


Configuration Options

LoggerMiddlewareOptions controls the middleware behavior. It extends LoggerOptions from @nextrush/log.

Middleware Options

PropertyTypeDescription
skip?(ctx: Context) => booleanSkip logging for matching requests
formatMessage?(ctx: Context, duration: number) => string= `${method} ${path}`Custom log message formatter
successLevel?LogLevel= "info"Log level for 2xx/3xx responses
clientErrorLevel?LogLevel= "warn"Log level for 4xx responses
serverErrorLevel?LogLevel= "error"Log level for 5xx responses
logRequestStart?boolean= true in dev, false in productionLog when request starts
correlationIdHeader?string= "x-request-id"Header name for correlation ID
generateCorrelationId?boolean= trueGenerate ID if not present in headers
context?string= "nextrush"Logger namespace prefix

Inherited from LoggerOptions

PropertyTypeDescription
minLevel?LogLevelMinimum log level to output
silent?boolean= falseSuppress console output (transports still fire)
pretty?booleanForce pretty printing regardless of environment
colors?booleanEnable/disable terminal colors
redact?boolean= true in productionRedact sensitive keys in log output
sensitiveKeys?string[]Additional keys to redact (merged with defaults)
transports?LogTransport[]Custom log transports
timestamps?booleanInclude timestamps in output
metadata?LogContextAdditional metadata for all log entries
samplingRate?numberSampling rate for debug logs in production (0–1)
env?'development' | 'test' | 'production'Environment preset
app.use(
  logger({
    minLevel: 'info',
    skip: (ctx) => ctx.path === '/health',
    logRequestStart: false,
    correlationIdHeader: 'x-request-id',
    context: 'api',
  })
);

Programmatic API (re-exported from @nextrush/log)

Beyond the middleware surface above, this package re-exports its entire underlying @nextrush/log library — useful for application code that needs structured logging outside a request (startup, background jobs, CLI scripts).

Configuration

PropertyTypeDescription
configure(config: Partial<GlobalLoggerConfig>) => voidSet global logger defaults (level, transports, redaction) for every logger created afterward.
configureFromEnv() => voidPopulate global config from environment variables (e.g. LOG_LEVEL).
getGlobalConfig() => GlobalLoggerConfigRead the current global configuration.
resetGlobalConfig() => voidReset global configuration to defaults — mainly for test isolation.
setGlobalLevel(level: LogLevel) => voidSet the minimum log level globally.
onConfigChange(cb: (config: GlobalLoggerConfig) => void) => () => voidSubscribe to global config changes; returns an unsubscribe function.

Transports

PropertyTypeDescription
createConsoleTransport(options?) => LogTransportThe default transport — writes to stdout/stderr.
createBatchTransport(transport: LogTransport, options?) => LogTransportBatches entries before forwarding, to reduce I/O overhead under high log volume.
createFilteredTransport(transport: LogTransport, predicate) => LogTransportWraps a transport to only forward entries matching a predicate.
createPredicateTransport(predicate, transport: LogTransport) => LogTransportAlias form of createFilteredTransport.
createRateLimitedTransport(transport: LogTransport, options: RateLimitOptions) => LogTransportCaps entries-per-window forwarded to the underlying transport.
createNamespaceRateLimitedTransport(transport: LogTransport, limits: NamespaceRateLimits) => LogTransportPer-namespace variant of the rate-limited transport.
addGlobalTransport(transport: LogTransport) => voidRegister an additional transport for every logger.
clearGlobalTransports() => voidRemove all globally registered transports.

Formatters

PropertyTypeDescription
formatJSON(entry: LogEntry) => stringNewline-delimited JSON — the default in production.
formatPrettyJSON(entry: LogEntry) => stringIndented, human-readable JSON.
formatPrettyTerminal(entry: LogEntry) => stringColorized terminal output — the default in development.
formatPrettyTimestamp(date: Date) => stringFormats a timestamp for the pretty-terminal formatter's header.
formatTimestamp(date: Date) => stringFormats a timestamp for the JSON formatters (ISO 8601).

Context (AsyncLocalStorage)

PropertyTypeDescription
runWithContext<T>(context: LogContext, fn: () => T) => TRuns fn with LogContext available to any logger call inside it, without explicit passing.
getAsyncContext() => LogContext | undefinedReads the current AsyncLocalStorage-backed context, if any.
getContextCorrelationId() => string | undefinedConvenience accessor for the correlation ID on the current context.
getContextMetadata() => Record<string, unknown> | undefinedReads arbitrary metadata attached to the current context.
isAsyncContextAvailable() => booleanWhether AsyncLocalStorage is supported in the current runtime.
createContextMiddleware() => MiddlewareA NextRush middleware that seeds runWithContext for the request lifecycle.

Redaction & serialization

PropertyTypeDescription
DEFAULT_SENSITIVE_KEYSreadonly string[]The built-in list of keys redacted by default (password, token, authorization, etc.).
mergeSensitiveKeys(...lists: string[][]) => string[]Combines the default list with custom keys.
shouldRedact(key: string, sensitiveKeys: string[]) => booleanWhether a given key matches the redaction list.
containsSensitivePattern(value: string) => booleanHeuristic check for values that look sensitive even under an unlisted key (e.g. a JWT-shaped string).
redactSensitiveValues(data: unknown, sensitiveKeys: string[]) => unknownDeep-redacts matching keys in an object before logging.
sanitizeContext(context: LogContext, sensitiveKeys: string[]) => LogContextApplies redaction to a LogContext object specifically.
safeSerialize(value: unknown) => unknownSerializes a value defensively — handles circular references and non-JSON-safe types.
serializeError(error: Error) => SerializedErrorConverts an Error into a plain, loggable object (message, stack, cause).

Levels & runtime

PropertyTypeDescription
LOG_LEVELSreadonly LogLevel[]Every level in ascending severity: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].
LOG_LEVEL_PRIORITYRecord<LogLevel, number>Numeric priority per level, used for minLevel comparisons.
compareLevels(a: LogLevel, b: LogLevel) => numberCompares two levels by priority.
isValidLogLevel(value: unknown) => value is LogLevelType guard for a LogLevel string.
parseLogLevel(value: string, fallback: LogLevel) => LogLevelParses an environment/config string into a LogLevel, falling back if invalid.
shouldLog(level: LogLevel, minLevel: LogLevel) => booleanWhether an entry at level clears the minLevel threshold.
isProductionBuild() => booleanWhether the current build/environment is production (drives several defaults).
isError(value: unknown) => value is ErrorType guard for Error-like values.
detectRuntime() => RuntimeEnvironmentDetects Node.js/Bun/Deno/browser/edge at the logging layer (independent of `@nextrush/runtime`).
getRuntime() => RuntimeEnvironmentCached accessor for the detected runtime.
getRuntimeInfo() => RuntimeInfoExtended runtime detail (name + version) for log metadata.
getEnvVar(name: string) => string | undefinedCross-runtime environment variable read (Node, Deno, Bun).
getProcessId() => number | undefinedProcess ID for log metadata, where available.
getTime() => numberHigh-resolution timestamp source used for duration calculations.
isNamespaceEnabled(namespace: string) => booleanWhether a given logger namespace currently passes the enable/disable filters below.
enableNamespaces(...patterns: string[]) => voidEnables logging for namespaces matching the given patterns.
disableNamespaces(...patterns: string[]) => voidDisables logging for namespaces matching the given patterns.
enableLogging() => voidGlobally re-enables logging after disableLogging().
disableLogging() => voidGlobally suppresses all log output — mainly for tests.
log(level: LogLevel, message: string, ...args) => voidThe lowest-level log call, bypassing a named Logger instance.
defaultLoggerILoggerThe library's own default logger instance (aliased from @nextrush/log's `logger`).
scopedLogger(namespace: string) => ILoggerAlias for creating a namespace-scoped logger, equivalent to createLogger(namespace).

Integration Example

Correlation IDs Across Services

Track requests through microservices using the correlation ID:

import { logger } from '@nextrush/logger';
import type { LoggerContext } from '@nextrush/logger';

app.use(logger());

app.use(async (ctx) => {
  const correlationId = (ctx as LoggerContext).log.getCorrelationId();

  const response = await fetch('https://api.internal/data', {
    headers: { 'x-request-id': correlationId ?? '' },
  });

  ctx.json(await response.json());
});

Do not use ctx.get('x-request-id') to retrieve a generated correlation ID. The middleware sets it on the response via ctx.set(). Use ctx.log.getCorrelationId() instead.

Logger Without Request Logging

Use attachLogger() when you need ctx.log but not automatic request/response logging:

import { attachLogger } from '@nextrush/logger';

app.use(attachLogger({ context: 'api' }));

app.use(async (ctx) => {
  ctx.log.info('Handler called');
  ctx.json({ ok: true });
});

Type Guards

import { hasLogger, getLogger } from '@nextrush/logger';

app.use(async (ctx) => {
  if (hasLogger(ctx)) {
    ctx.log.info('Logger available');
  }

  // Get logger with automatic fallback
  const log = getLogger(ctx, 'fallback-context');
  log.info('Works with or without middleware');
});

Standalone Logger

Create loggers outside of middleware context:

import { createLogger } from '@nextrush/logger';

const log = createLogger('MyService');
log.info('Server starting');
log.error('Connection failed', new Error('timeout'));

Compatibility

RuntimeSupportedNotes
Node.js >=22YesESM-only
Bun / DenoYes@nextrush/log detects these runtimes explicitly (detectRuntime()) and uses AsyncLocalStorage-based context where available
Edge / Cloudflare Workers / browsersPartial@nextrush/log falls back to a non-AsyncLocalStorage context path per its own runtime detection; correlation-ID propagation through this package's middleware still works, since it does not depend on AsyncLocalStorage itself

@nextrush/core is an optional peer dependency — only needed for the Middleware/Context types it references at compile time; the middleware itself only needs a NextRush-shaped Context.


Common Mistakes

Placing logger middleware after route handlers. The middleware must run before handlers to attach ctx.log. Register it early in the middleware chain.

Skipping health checks without the skip option. Health check endpoints generate high-volume logs with no diagnostic value. Use skip: (ctx) => ctx.path === '/health'.

Using redactKeys instead of sensitiveKeys. The option inherited from @nextrush/log is sensitiveKeys, not redactKeys.


Troubleshooting

ctx.log is undefined — The logger middleware did not run before your handler. Ensure app.use(logger()) is registered before route handlers. If using skip, verify your route is not being skipped.

Correlation ID not appearing in logs — Set generateCorrelationId: true (the default). If using a custom header name, ensure correlationIdHeader matches the header your upstream service sends.

Too many logs in production — Set minLevel: 'info' and logRequestStart: false to reduce volume. Use skip to exclude health checks and static asset paths.


Was this helpful?

On this page