X Enterprises

fastify-xlogger

Fastify plugin for standardized logging with Pino — request context, secret redaction, business event logging, boundary logging, and background job correlation.

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).

OptionTypeDefaultRequiredDescription
activebooleantrueNoSet to false to skip plugin registration entirely.
serviceNamestring"fastify-app"NoService identifier stored in config.
environmentstring"development"NoEnvironment name stored in config.
redactPathsstring[][]NoAdditional Pino paths to redact (extends the built-in defaults).
redactClobberbooleanfalseNoReplace the default redact list entirely instead of extending it.
includeRequestBodybooleanfalseNoAttach the parsed request body to the per-request http.response log line (debug level; flows through Pino redaction).
includeResponseBodybooleanfalseNoAttach the response body to the per-request http.response log line (debug level; flows through Pino redaction).
contextExtractorfunctionnullNo(request) => object — add custom fields to every request context.
enableBoundaryLoggingbooleantrueNoEmit 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:

FieldSource
requestIdrequest.id
routerequest.routeOptions.url
methodrequest.method
orgIdx-org-id / x-tenant-id header, or request.user.orgId / organizationId / tenantId
userIdx-user-id header, or request.user.id / userId / sub
traceId / spanIdtraceparent 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, and fail helpers.
  • 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:

ErrorCause
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:

ErrorCause
[xLogger] logEvent requires a string 'event' parameterlogEvent() called without a string event.
[xLogger] logBoundary requires a string 'vendor' parameterlogBoundary() called without vendor.
[xLogger] logBoundary requires a string 'operation' parameterlogBoundary() called without operation.
[xLogger] createBoundaryLogger requires a string 'vendor' parametercreateBoundaryLogger() called without vendor.
[xLogger] createBoundaryLogger requires a string 'operation' parametercreateBoundaryLogger() called without operation.
[xLogger] createJobContext requires a string 'jobName' parametercreateJobContext() 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, { ... }).

VariablePassed in asDescription
SERVICE_NAMEserviceName optionService name on every log entry.
NODE_ENVenvironment optionIn getLoggerOptions(), drives the default log level (info when "production", debug otherwise) and transport (JSON in production, pino-pretty otherwise).
BETTERSTACK_SOURCE_TOKENtransport.options.sourceTokenSource 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:

  1. onRequest — creates a Pino child logger (request.contextLog) for every request by calling extractContext — which reads request.user, standard headers (x-org-id, x-tenant-id, x-user-id), and the W3C traceparent header — and binding those fields to the child. When enableBoundaryLogging is on (default), also emits a boundary.request.start debug event.
  2. onSend — registered only when includeResponseBody is enabled; captures the response payload for logging. Stream payloads are not buffered.
  3. onResponse — writes a canonical http.response event with status code, elapsed time, and request context; level is error for 5xx, warn for 4xx, info otherwise — debug when body logging is enabled, with requestBody / responseBody attached. Emits a boundary.request.end debug event when enableBoundaryLogging is 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)
Copyright © 2026