@nextrush/logger
v1.0.0
Published
Request logging middleware for NextRush - wraps @nextrush/log
Maintainers
Readme
@nextrush/logger
Request logging middleware for NextRush -- attaches a correlation-ID-aware logger to every request context and re-exports the full @nextrush/log API.
| | |
| --- | --- |
| Purpose | Attach a request-scoped, correlation-ID-aware logger to ctx.log and log every request's method/path/status/duration |
| Package type | Middleware |
| Status | Stable |
| Included in nextrush? | No -- standalone install. Not re-exported from nextrush or nextrush/class. |
| Support tier | Public -- middleware/registrar (stable) -- see ADR-0005 |
| Maintenance | Active |
| Runtime | Universal -- Node, Bun, Deno, Edge for the middleware itself (see Compatibility for the underlying logger's per-runtime notes) |
| Requires | Node >=22, ESM-only, TypeScript >=5.x |
| Introduced | v1.0.0 |
Highlights
- The middleware itself has zero third-party runtime dependencies beyond its two
@nextrush/*deps; it depends on the standalone@nextrush/logpackage for the actual logging engine (see Package relationships) - ESM-only, tree-shakable, side-effect-free (
sideEffects: false) - Fully typed, strict TypeScript, zero
any - Structured JSON output by default in production; colorized pretty-terminal output by default in development -- both formats come from
@nextrush/log, not reimplemented here
The problem . When to use . Installation . Quick start . Capabilities . Mental model . Common tasks . API overview . Options . Compatibility . Troubleshooting . FAQ . Package relationships . Architecture . Resources
The problem
Logging a request by hand means remembering to capture the same fields every time -- method, path, status, duration -- and getting them wrong is easy: a duration measured before the handler runs instead of after, a status read before the handler sets it, or a stray console.log(userObject) that puts a password or session token straight into a log aggregator with no code review catching it.
// TODAY, without this package -- looks fine, has real gaps:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
console.log(`${ctx.method} ${ctx.path} ${ctx.status}`, Date.now() - start);
// No correlation ID -- can't tie this line to the request that produced it
// downstream. No structured fields -- a log aggregator has to regex this.
// No redaction -- ctx.query or a logged object can carry a token straight
// into the log stream with nothing stopping it here.
});When to use
Use @nextrush/logger if:
- You want every request logged with a correlation ID, method, path, status, and duration without writing that boilerplate per route
- You want
ctx.logavailable in handlers, pre-scoped to the request's correlation ID - You want production logs as structured JSON (for a log aggregator) and development logs as readable colored text, without hand-switching the format
Reach for something else if:
- You only need application-level logging with no request middleware or correlation-ID wiring -- install
@nextrush/logdirectly; this package re-exports it in full but adds a request layer on top - You need response-time headers on the wire (
X-Response-Time) rather than log lines -- see@nextrush/timer - You need the correlation ID itself generated/propagated as a header for services that don't log -- see
@nextrush/request-id; the two packages solve overlapping but distinct problems and can be used together
Installation
pnpm add @nextrush/logger
# npm i @nextrush/logger . yarn add @nextrush/logger . bun add @nextrush/logger[!NOTE]
@nextrush/loggeris not re-exported by thenextrushmeta package -- install and import it directly, as shown above. It depends on@nextrush/log(installed automatically).
Quick start
import { createApp, listen } from 'nextrush';
import { logger } from '@nextrush/logger';
const app = createApp();
app.use(logger());
app.get('/users', (ctx) => {
ctx.log.info('Fetching users');
ctx.json({ users: [] });
});
listen(app, 8080);Every request now gets a correlation ID (generated, or read from x-request-id if the client sent one), a request-scoped logger at ctx.log, and a completion log line carrying method/path/status/duration -- all three without any code in the handler.
Capabilities
Request logging
- Logs request completion (method, path, status, duration in ms) for every request, unless
skip()excludes it - Logs request start too, but only by default in non-production (
logRequestStartdefault followsisProductionBuild()) - Picks the log level per response automatically:
successLevelfor 2xx/3xx,clientErrorLevelfor 4xx,serverErrorLevelfor 5xx (info/warn/errorby default) -- an uncaught error is always logged via.error()regardless of status, then rethrown
Correlation ID
- Reads the correlation ID from a request header (
x-request-idby default, configurable viacorrelationIdHeader) - Generates one with
crypto.randomUUID()when missing and not disabled, falling back to a timestamp+random string on runtimes withoutcrypto.randomUUID - Echoes the correlation ID back on the same response header name it was read from/generated for
Context integration
- Attaches a correlation-ID-scoped logger to
ctx.log(vialogger()or the lighterattachLogger(), which skips request-completion logging) hasLogger()/getLogger()give a safe way to readctx.logwhen a handler can't be sure the middleware ran first
Re-exported logging engine (@nextrush/log)
- Every export of
@nextrush/log--createLogger, transports, formatters, serializers, redaction utilities, runtime detection,AsyncLocalStorage-based context propagation -- is re-exported from this package's entry point, so application code depends on one package instead of two
Mental model
logger() does three things around your handler: read-or-generate a correlation ID before next(), attach ctx.log before next(), and log one completion line after next() resolves (or throws).
request --> logger() --> read/generate correlation ID --> ctx.log = scoped logger
|
await next() (your handlers run)
|
request completes or throws
|
one log line: method, path, status, durationRule: the completion log line is written in a finally block, so it fires whether the handler succeeds, sends an error response, or throws -- there is no code path through logger() that skips it once next() has been called.
[!TIP] The full request-to-log-line sequence, and exactly where redaction happens in that sequence, are in
ARCHITECTURE.md.
Common tasks
Log every request with defaults
import { logger } from '@nextrush/logger';
app.use(logger());Skip noisy paths
app.use(logger({
skip: (ctx) => ctx.path === '/health' || ctx.path === '/metrics',
}));Attach ctx.log without per-request completion logging
import { attachLogger } from '@nextrush/logger';
app.use(attachLogger({ context: 'api' }));
app.use(async (ctx) => {
ctx.log.info('Handler called');
ctx.json({ ok: true });
});Read ctx.log defensively when middleware order is uncertain
import { getLogger, hasLogger } from '@nextrush/logger';
app.use(async (ctx) => {
if (hasLogger(ctx)) {
ctx.log.info('Logger was already attached');
}
const log = getLogger(ctx, 'fallback');
log.info('Always works, even without the middleware');
});Redact custom sensitive keys on top of the defaults
import { logger } from '@nextrush/logger';
app.use(logger({
redact: true,
sensitiveKeys: ['internalUserId', 'billingRef'],
}));sensitiveKeys is merged with @nextrush/log's built-in default list (DEFAULT_SENSITIVE_KEYS) via mergeSensitiveKeys -- you are extending the list, not replacing it.
API overview
The sealed public surface (ADR-0005). Everything from @nextrush/log is also re-exported in full;
only this package's own additions are listed here -- see Re-exports from @nextrush/log.
| Export | Signature | Since | Stability | Description |
| ------ | --------- | ----- | --------- | ----------- |
| logger | (options?: LoggerMiddlewareOptions) => Middleware | 1.0.0 | Stable | Request logging middleware. Attaches ctx.log and logs one completion line per request. |
| attachLogger | (options?: LoggerMiddlewareOptions) => Middleware | 1.0.0 | Stable | Attaches ctx.log only -- no completion logging. |
| hasLogger | (ctx: Context) => ctx is LoggerContext | 1.0.0 | Stable | Type guard for whether ctx.log is present and looks like a logger. |
| getLogger | (ctx: Context, fallbackContext?: string) => ILogger | 1.0.0 | Stable | Returns ctx.log if present, else a fresh logger for fallbackContext (default 'nextrush'). |
| type LoggerContext | Context & { log: ILogger } | 1.0.0 | Stable | Context narrowed to include log. |
| type LoggerMiddlewareOptions | LoggerOptions & { skip?, formatMessage?, successLevel?, clientErrorLevel?, serverErrorLevel?, logRequestStart?, correlationIdHeader?, generateCorrelationId?, context? } | 1.0.0 | Stable | Options for logger()/attachLogger(). Extends @nextrush/log's LoggerOptions. |
Re-exports from @nextrush/log
createLogger, logger as defaultLogger, log, Logger, configure, configureFromEnv, setGlobalLevel, enableLogging, disableLogging, enableNamespaces, disableNamespaces, createConsoleTransport, createBatchTransport, createFilteredTransport, createPredicateTransport, createRateLimitedTransport, createNamespaceRateLimitedTransport, addGlobalTransport, clearGlobalTransports, formatJSON, formatPrettyJSON, formatPrettyTerminal, formatTimestamp, formatPrettyTimestamp, safeSerialize, serializeError, isError, redactSensitiveValues, containsSensitivePattern, shouldRedact, mergeSensitiveKeys, sanitizeContext, DEFAULT_SENSITIVE_KEYS, detectRuntime, getRuntime, getEnvVar, isProductionBuild, getProcessId, runWithContext, getAsyncContext, getContextCorrelationId, getContextMetadata, isAsyncContextAvailable, createContextMiddleware, shouldLog, compareLevels, isValidLogLevel, parseLogLevel, LOG_LEVELS, LOG_LEVEL_PRIORITY, plus every type (LogLevel, LogEntry, LogContext, LoggerOptions, ILogger, LogTransport, RuntimeEnvironment, RuntimeInfo, SerializedError, PerformanceMetrics, Timer, BatchTransport, BatchTransportOptions, AsyncLogContext, RateLimitOptions, RateLimitStats, NamespaceRateLimits). See @nextrush/log for full documentation of these.
Options
Every default below is read directly from src/index.ts (this package) or @nextrush/log's LoggerOptions resolution for the rows marked "via @nextrush/log".
| Option | Type | Required | Default | Security-sensitive | Description |
| ------ | ---- | -------- | ------- | ------------------ | ----------- |
| skip | (ctx: Context) => boolean | No | undefined | No | When it returns true, logger() calls next() and returns immediately -- no correlation ID, no ctx.log, no completion line for that request. |
| formatMessage | (ctx: Context, duration: number) => string | No | `${method} ${path}` | No | Overrides the completion log line's message text only -- the structured fields (method, path, status, duration) are still attached separately. |
| successLevel | LogLevel | No | 'info' | No | Level used for 2xx/3xx completion logs. |
| clientErrorLevel | LogLevel | No | 'warn' | No | Level used for 4xx completion logs. |
| serverErrorLevel | LogLevel | No | 'error' | No | Level used for 5xx completion logs. |
| logRequestStart | boolean | No | !isProductionBuild() | No | Whether a debug-level "Request started" line is logged before next(). |
| correlationIdHeader | string | No | 'x-request-id' | No | Header name read for an incoming correlation ID, and written back on the response. |
| generateCorrelationId | boolean | No | true | No | Whether to generate a correlation ID (via crypto.randomUUID(), falling back to timestamp+random) when the header is absent. |
| context | string | No | 'nextrush' | No | The logger context/name prefix passed to createLogger(). |
| redact (via @nextrush/log) | boolean | No | true in production, false in development/test | Yes | Enables key-based and pattern-based redaction inside @nextrush/log's serializer -- see Architecture for exactly what gets redacted and when. |
| sensitiveKeys (via @nextrush/log) | string[] | No | [] (merged with DEFAULT_SENSITIVE_KEYS) | Yes | Additional key names to redact, on top of the ~60 built-in defaults (password, token, authorization, cookie, ssn, credit-card/bank fields, and similar). |
| pretty (via @nextrush/log) | boolean | No | true in development/test, false in production | No | Colorized multi-line terminal output vs. single-line JSON. See Mental model and Architecture. |
| minLevel (via @nextrush/log) | LogLevel | No | 'trace' in dev/test; 'info' (or 'debug' if ENABLE_DEBUG_LOGS/DEBUG is set) in production | No | The floor below which a log call is dropped before it reaches formatting/redaction at all. |
Compatibility
Requirements
| Requirement | Version | | ----------- | ------- | | NextRush | 3.x | | Node.js | >=22 | | TypeScript | >=5.x |
Runtimes
| Runtime | Supported | Notes |
| ------- | --------- | ----- |
| Node.js >=22 | Yes | ESM-only |
| Bun / Deno | Yes / Yes | @nextrush/log detects these runtimes explicitly (detectRuntime()) and uses AsyncLocalStorage-based context where available |
| Edge / Cloudflare Workers / browsers | Partial | @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 |
Integration
- Peer dependencies:
@nextrush/core(optional -- only needed for theMiddleware/Contexttypes it references at compile time; the middleware itself only needs a NextRush-shapedContext). - Depends on:
@nextrush/log(a separate, standalone npm package -- see Package relationships),@nextrush/types. - Works with:
@nextrush/request-idfor header-based correlation-ID propagation to downstream services;@nextrush/timerfor response-time headers alongside logged duration. - Incompatible with: none directly -- both packages can independently read/write the same
x-request-id-style header if configured to use the same header name; the last one to run wins on the response header (thoughlogger()'s own correlation ID is unaffected either way, since it reads before writing).
[!IMPORTANT] NextRush is ESM-only, permanently -- no CommonJS build. On Node >=22, CommonJS consumers can
require()this ESM package natively. See the Module Format Policy.
Troubleshooting
Cause: @nextrush/log's pretty option defaults to true only in development/test; in a production environment (detected via NODE_ENV or isProductionBuild()) it defaults to false, switching the formatter from formatPrettyTerminal to formatJSON. Fix: if you want pretty output in a production-labeled environment anyway, pass pretty: true explicitly to logger().
Cause: the key name matched (case-insensitively, by substring) one of @nextrush/log's DEFAULT_SENSITIVE_KEYS, or the value matched a built-in SSN/credit-card/bank-account pattern -- this happens automatically whenever redact is true (the production default), independent of what you named the field. Fix: this is working as intended for genuinely sensitive fields. If a field is being redacted by mistake (e.g. a key merely containing "key" or "hash" as a substring), rename the field, or explicitly pass redact: false if you have already reviewed the data for sensitivity elsewhere.
Cause: @nextrush/request-id and @nextrush/logger each generate/read a correlation ID independently -- if both are mounted and configured with different header names (or one runs before the client's header is set), each will generate its own ID. Fix: point both packages at the same correlationIdHeader/equivalent option, and make sure whichever middleware runs first is the one whose generated ID you want propagated.
Cause: neither logger() nor attachLogger() ran before the handler -- ctx.log is only attached by those two middleware, not by Context itself. Fix: register app.use(logger()) (or attachLogger()) before any route that reads ctx.log, or use getLogger(ctx) in handlers that may run without it, which returns a fallback logger instead of throwing.
FAQ
Can I use @nextrush/log without the middleware?
Yes -- install @nextrush/log directly for application-level logging with no request/correlation-ID layer. This package is a superset: everything @nextrush/log exports is re-exported here too, so most applications only need to depend on @nextrush/logger.
Is sensitive data redacted from logs automatically?
Yes, but only by default in production. @nextrush/log's redact option (re-exported and configurable through this package's logger()/attachLogger() options) defaults to true in production and false in development/test, and applies both key-based matching (~60 default sensitive key names, extendable via sensitiveKeys) and pattern-based matching (SSN, credit-card, and bank-account-shaped strings) to logged data. See Architecture for exactly where this runs in the request lifecycle.
Why ESM-only? See the Module Format Policy.
Does it work on Bun / Deno / Edge?
Yes for the middleware itself. @nextrush/log's context-propagation mechanism varies by runtime (see Compatibility) -- correlation-ID handling in this package does not depend on that mechanism, so it behaves identically everywhere.
Package relationships
depends on @nextrush/log (the logging engine -- a separate published package)
@nextrush/logger ------------------->
depends on @nextrush/types (Context / Middleware contracts, types only)
often used with @nextrush/request-id, @nextrush/timer- Depends on:
@nextrush/log-- the standalone logging engine (levels, transports, formatters, redaction, runtime detection); this package wraps it with a request middleware and re-exports its full API.@nextrush/types--Context/Middlewarecontracts, types only. - Often used with:
@nextrush/request-id-- a narrower package focused specifically on generating/propagating a correlation ID header, useful when you want that behavior without pulling in the full logging engine.@nextrush/timer-- response-time headers on the wire, complementing this package's logged duration field. - Usually used next:
@nextrush/health-- liveness/readiness endpoints, commonly excluded from request logging viaskip. - Alternative: none within NextRush for structured request logging --
@nextrush/logalone covers application logging without the request layer.
Architecture
Maintaining or contributing to this package? The internal design -- the request-to-log-line
sequence, exactly where redaction and format selection happen, and the decisions and trade-offs
behind them (with diagrams) -- is in ARCHITECTURE.md.
Resources
- Learn -- Documentation . Architecture . RFCs
- Changelog -- CHANGELOG.md
- Report an issue -- GitHub Issues
- Contribute -- CONTRIBUTING.md
MIT (c) Tanzim Hossain
