fastify-xlogger
fastify-xlogger
Standardized logging for Fastify v5 built on the built-in Pino logger. Register once to get automatic request context, secret redaction at the Pino level, canonical log schema, boundary logging for external API calls, and background job correlation.
Installation
npm install @xenterprises/fastify-xlogger fastify@5
Quick Start
import Fastify from "fastify";
import xLogger, { getLoggerOptions } from "@xenterprises/fastify-xlogger";
const fastify = Fastify({
logger: getLoggerOptions({ serviceName: "my-api" }),
});
await fastify.register(xLogger, { serviceName: "my-api" });
fastify.get("/users/:id", async (request) => {
request.contextLog.info({ userId: request.params.id }, "Fetching user");
fastify.xLogger.logEvent({
event: "user.fetched",
data: { userId: request.params.id },
request,
});
return { id: request.params.id };
});
Options
All configuration is passed at registration. The plugin never reads process.env — pass environment-derived values in yourself (see Environment Variables).
| Option | Type | Default | Required | Description |
|---|---|---|---|---|
active | boolean | true | No | Set to false to skip plugin registration entirely. |
serviceName | string | "fastify-app" | No | Service identifier stored in config. |
environment | string | "development" | No | Environment name stored in config. |
redactPaths | string[] | [] | No | Additional Pino paths to redact (extends the built-in defaults). |
redactClobber | boolean | false | No | Replace the default redact list entirely instead of extending it. |
includeRequestBody | boolean | false | No | Attach the parsed request body to the per-request http.response log line (debug level; flows through Pino redaction). |
includeResponseBody | boolean | false | No | Attach the response body to the per-request http.response log line (debug level; flows through Pino redaction). |
contextExtractor | function | null | No | (request) => object — add custom fields to every request context. |
enableBoundaryLogging | boolean | true | No | Emit boundary.request.start / boundary.request.end debug events around each request. |
request.contextLog
A Pino child logger automatically created for every request with canonical context fields:
| Field | Source |
|---|---|
requestId | request.id |
route | request.routeOptions.url |
method | request.method |
orgId | x-org-id / x-tenant-id header, or request.user.orgId / organizationId / tenantId |
userId | x-user-id header, or request.user.id / userId / sub |
traceId / spanId | traceparent header (OpenTelemetry W3C format) |
fastify.get("/orders/:id", async (request) => {
request.contextLog.info({ orderId: request.params.id }, "Processing order");
});
Methods
fastify.xLogger decorator
- logEvent — Log a structured business event with canonical schema.
- logBoundary — Log an external API call with vendor, operation, duration, and status.
- createBoundaryLogger — Create a timed boundary logger that captures duration automatically.
- createJobContext — Create a correlated child logger for background jobs with
start,complete, andfailhelpers. - extractContext — Extract the canonical context object from a Fastify request.
The decorator also exposes config (resolved plugin configuration), levels (log level constants), and redactPaths (effective redact paths).
Exported function
- getLoggerOptions — Build Pino logger options (redaction, serializers, transport) for use when creating the Fastify instance.
Default Redacted Paths
Auth headers: req.headers.authorization, req.headers.cookie, req.headers['set-cookie'], req.headers['x-api-key']
Secret fields: password, token, secret, apiKey, api_key, accessToken, access_token, refreshToken, refresh_token, privateKey, private_key
PII / payment: cardNumber, card_number, cvv, ssn, creditCard
Nested: *.password, *.token, *.secret, *.apiKey, *.api_key
Use redactPaths to extend, or redactClobber: true to replace entirely.
Body logging and redaction
When includeRequestBody / includeResponseBody are enabled, bodies are attached to the per-request http.response log line as requestBody / responseBody, and that line drops to debug level for non-error responses (4xx/5xx keep warn/error).
Redaction is applied by Pino at log time, against the logger options you created with getLoggerOptions() (or your own redact config). Bodies are logged as structured objects, so they flow through the same redaction paths: a body field one level deep (e.g. requestBody.password, responseBody.token) is caught by the default wildcard paths (*.password, *.token, ...). Add your own patterns via redactPaths — e.g. "*.creditCard" covers requestBody.creditCard.
Bodies are deep-copied before logging with safety caps — strings truncated at 2048 chars, nesting capped at depth 5, arrays capped at 100 items, and circular references replaced with "[Circular]" — so logging a body can never crash the process. Stream payloads are not buffered; they log as "[Stream]".
Error Reference
Startup Errors
Registration fails fast when options are invalid — messages name the plugin, the option, and show a correct usage example:
| Error | Cause |
|---|---|
xlogger: option `redactPaths` must be an array of strings, e.g. `app.register(xLogger, { redactPaths: ['req.headers.x-custom-secret'] })` | redactPaths is not an array of strings. |
xlogger: option `contextExtractor` must be a function, e.g. `app.register(xLogger, { contextExtractor: (request) => ({ orgId: request.headers['x-org-id'] }) })` | contextExtractor is not a function. |
xlogger: option `serviceName` must be a string, e.g. `app.register(xLogger, { serviceName: "my-api" })` | serviceName is not a string. |
xlogger: option `environment` must be a string, e.g. `app.register(xLogger, { environment: "production" })` | environment is not a string. |
xlogger: option `includeRequestBody` must be a boolean, e.g. `app.register(xLogger, { includeRequestBody: true })` | includeRequestBody is not a boolean. |
xlogger: option `includeResponseBody` must be a boolean, e.g. `app.register(xLogger, { includeResponseBody: true })` | includeResponseBody is not a boolean. |
xlogger: option `redactClobber` must be a boolean, e.g. `app.register(xLogger, { redactClobber: true, redactPaths: ['onlyThis'] })` | redactClobber is not a boolean. |
xlogger: option `enableBoundaryLogging` must be a boolean, e.g. `app.register(xLogger, { enableBoundaryLogging: false })` | enableBoundaryLogging is not a boolean. |
Runtime Errors
The decorator methods validate their required arguments at call time:
| Error | Cause |
|---|---|
[xLogger] logEvent requires a string 'event' parameter | logEvent() called without a string event. |
[xLogger] logBoundary requires a string 'vendor' parameter | logBoundary() called without vendor. |
[xLogger] logBoundary requires a string 'operation' parameter | logBoundary() called without operation. |
[xLogger] createBoundaryLogger requires a string 'vendor' parameter | createBoundaryLogger() called without vendor. |
[xLogger] createBoundaryLogger requires a string 'operation' parameter | createBoundaryLogger() called without operation. |
[xLogger] createJobContext requires a string 'jobName' parameter | createJobContext() called without jobName. |
Environment Variables
The plugin never reads process.env. The variables below are a consumer-side convention: your application reads them and passes the values into getLoggerOptions() and app.register(xLogger, { ... }).
| Variable | Passed in as | Description |
|---|---|---|
SERVICE_NAME | serviceName option | Service name on every log entry. |
NODE_ENV | environment option | In getLoggerOptions(), drives the default log level (info when "production", debug otherwise) and transport (JSON in production, pino-pretty otherwise). |
BETTERSTACK_SOURCE_TOKEN | transport.options.sourceToken | Source token for the optional @logtail/pino (Betterstack) transport. |
const fastify = Fastify({
logger: getLoggerOptions({
serviceName: process.env.SERVICE_NAME, // consumer owns env access
environment: process.env.NODE_ENV,
transport: {
target: "@logtail/pino", // npm i @logtail/pino (optional peer)
options: { sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN },
},
}),
});
await fastify.register(xLogger, {
serviceName: process.env.SERVICE_NAME,
environment: process.env.NODE_ENV,
});
How It Works
On registration, the plugin validates all options and decorates fastify.xLogger with configuration and utility methods. It registers up to three hooks:
onRequest— creates a Pino child logger (request.contextLog) for every request by callingextractContext— which readsrequest.user, standard headers (x-org-id,x-tenant-id,x-user-id), and the W3Ctraceparentheader — and binding those fields to the child. WhenenableBoundaryLoggingis on (default), also emits aboundary.request.startdebug event.onSend— registered only whenincludeResponseBodyis enabled; captures the response payload for logging. Stream payloads are not buffered.onResponse— writes a canonicalhttp.responseevent with status code, elapsed time, and request context; level iserrorfor 5xx,warnfor 4xx,infootherwise —debugwhen body logging is enabled, withrequestBody/responseBodyattached. Emits aboundary.request.enddebug event whenenableBoundaryLoggingis on.
The utility methods are stateless — they write to request.log when a request is provided, or to fastify.log otherwise. The exported getLoggerOptions() configures Pino's redaction, serializers, and transport at Fastify instantiation time.
AI Context
package: "@xenterprises/fastify-xlogger"
type: fastify-plugin
use-when: Structured logging with request context, secret redaction, boundary logging for external APIs, and background job correlation
decorator: fastify.xLogger (config, logEvent, logBoundary, createBoundaryLogger, createJobContext, extractContext, levels, redactPaths)
request-decorator: request.contextLog — auto-created Pino child per request with requestId, userId, orgId, traceId
exported: getLoggerOptions() — call at Fastify instantiation with logger option
env: plugin reads no env vars — consumer passes SERVICE_NAME / NODE_ENV / BETTERSTACK_SOURCE_TOKEN values in as options (consumer convention)
