@xenterprises/fastify-xlogger
v1.2.1
Published
Fastify plugin for standardized logging with Pino - context, redaction, and canonical schema
Readme
@xenterprises/fastify-xlogger
A Fastify plugin for standardized logging with Pino. Provides automatic request context, secret redaction, canonical log schema, boundary logging for external APIs, and background job correlation.
Installation
npm install @xenterprises/fastify-xloggerQuick 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, reply) => {
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 };
});Plugin Options
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| active | boolean | true | No | Enable/disable the plugin |
| serviceName | string | process.env.SERVICE_NAME \|\| "fastify-app" | No | Service name for logs |
| environment | string | process.env.NODE_ENV \|\| "development" | No | Environment name |
| redactPaths | string[] | [] | No | Additional paths to redact (extends defaults) |
| redactClobber | boolean | false | No | Replace default redact paths instead of extending |
| includeRequestBody | boolean | false | No | Include request body in logs |
| includeResponseBody | boolean | false | No | Include response body in logs |
| contextExtractor | function | null | No | Custom function to extract additional context from request |
| enableBoundaryLogging | boolean | true | No | Enable boundary logging helpers |
getLoggerOptions(options) — Exported Function
Returns Pino logger options for Fastify initialization. Use this when creating the Fastify instance.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| level | string | "info" (prod) / "debug" (dev) | Log level |
| serviceName | string | process.env.SERVICE_NAME \|\| "fastify-app" | Service name in base object |
| redactPaths | string[] | [] | Additional paths to redact |
| pretty | boolean | false | Force pretty printing (uses pino-pretty) |
| transport | object | undefined | Custom Pino transport config (e.g. @logtail/pino) |
import { getLoggerOptions } from "@xenterprises/fastify-xlogger";
const fastify = Fastify({
logger: getLoggerOptions({
level: "debug",
serviceName: "my-api",
transport: {
target: "@logtail/pino",
options: { sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN },
},
}),
});Decorated Properties
| Decorator | Type | Description |
|-----------|------|-------------|
| fastify.xlogger.config | object | Plugin configuration |
| fastify.xlogger.extractContext(request) | function | Extract context object from request |
| fastify.xlogger.logEvent(params) | function | Log a business event |
| fastify.xlogger.logBoundary(params) | function | Log an external API call |
| fastify.xlogger.createBoundaryLogger(vendor, op, req) | function | Create timed boundary logger |
| fastify.xlogger.createJobContext(params) | function | Create background job context |
| fastify.xlogger.levels | object | Log level constants ({ fatal: 60, error: 50, ... }) |
| fastify.xlogger.redactPaths | string[] | Configured redact paths |
| request.contextLog | Logger | Child Pino logger with request context |
Features
Automatic Request Context
Every request gets a child logger (request.contextLog) with:
requestId— Unique request identifierroute— Route patternmethod— HTTP methodorgId— Fromx-org-id,x-tenant-idheaders, orrequest.user.orgId/organizationId/tenantIduserId— Fromx-user-idheader, orrequest.user.id/userId/subtraceId/spanId— Fromtraceparentheader (OpenTelemetry)
Secret Redaction
Default redacted paths:
- Headers:
authorization,cookie,set-cookie,x-api-key - Fields:
password,token,secret,apiKey,api_key,accessToken,refreshToken,privateKey - PII:
cardNumber,cvv,ssn,creditCard - Nested:
*.password,*.token,*.secret,*.apiKey,*.api_key
Business Event Logging — logEvent(params)
fastify.xlogger.logEvent({
event: "user.created", // Required: event name
msg: "User was created", // Optional: human-readable message
level: "info", // Optional: log level (default: "info")
data: { email: "[email protected]" }, // Optional: additional data
request, // Optional: adds request context
});Boundary Logging — logBoundary(params)
Log external API calls:
fastify.xlogger.logBoundary({
vendor: "stripe", // Required: service name
operation: "createCustomer", // Required: operation name
externalId: "cus_123", // Optional
durationMs: 150, // Optional
statusCode: 200, // Optional
success: true, // Optional (default: true)
retryCount: 0, // Optional
metadata: {}, // Optional
err: null, // Optional
request, // Optional
});Timed Boundary Logger — createBoundaryLogger(vendor, operation, request)
Automatically tracks call duration:
const boundary = fastify.xlogger.createBoundaryLogger("stripe", "charge", request);
try {
const result = await stripe.charges.create({ amount });
boundary.success({ externalId: result.id, statusCode: 200 });
} catch (err) {
boundary.retry(); // increment retry counter
boundary.fail(err, { statusCode: err.statusCode });
}Background Job Correlation — createJobContext(params)
const job = fastify.xlogger.createJobContext({
jobName: "processPayments", // Required
requestId: "req_123", // Optional: correlate to original request
orgId: "org_456", // Optional
userId: "user_789", // Optional
correlationId: "corr_abc", // Optional: auto-generated if not provided
});
job.start({ itemCount: 10 });
job.complete({ processed: 10 });
job.fail(err, { retried: 3 });
job.log.info("Custom log within job context");Custom Transports
Send logs to Betterstack/Logtail or other Pino transports:
npm install @logtail/pinoconst fastify = Fastify({
logger: getLoggerOptions({
transport: {
target: "@logtail/pino",
options: { sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN },
},
}),
});Multiple transports:
const fastify = Fastify({
logger: getLoggerOptions({
transport: {
targets: [
{ target: "@logtail/pino", options: { sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN } },
{ target: "pino/file", options: { destination: "/var/log/app.log" } },
],
},
}),
});Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| SERVICE_NAME | No | Service name (fallback if serviceName option not set) |
| NODE_ENV | No | Environment name — controls log level (info in production, debug otherwise) and formatting (JSON in production, pretty-print otherwise) |
| BETTERSTACK_SOURCE_TOKEN | No | Betterstack/Logtail source token (if using @logtail/pino transport) |
Error Reference
| Error Message | When |
|---------------|------|
| [xLogger] redactPaths must be an array of strings | redactPaths option is not an array |
| [xLogger] contextExtractor must be a function | contextExtractor option is not a function |
| [xLogger] serviceName must be a string | serviceName option is not a string |
| [xLogger] environment must be a string | environment option is not a string |
| [xLogger] includeRequestBody must be a boolean | includeRequestBody option is not a boolean |
| [xLogger] includeResponseBody must be a boolean | includeResponseBody option is not a boolean |
| [xLogger] redactClobber must be a boolean | redactClobber option is not a boolean |
| [xLogger] enableBoundaryLogging must be a boolean | enableBoundaryLogging option is not a boolean |
| [xLogger] logEvent requires a string 'event' parameter | logEvent() called without a string event |
| [xLogger] logBoundary requires a string 'vendor' parameter | logBoundary() called without a string vendor |
| [xLogger] logBoundary requires a string 'operation' parameter | logBoundary() called without a string operation |
| [xLogger] createBoundaryLogger requires a string 'vendor' parameter | createBoundaryLogger() called without a string vendor |
| [xLogger] createBoundaryLogger requires a string 'operation' parameter | createBoundaryLogger() called without a string operation |
| [xLogger] createJobContext requires a string 'jobName' parameter | createJobContext() called without a string jobName |
Log Levels
| Level | Value | Use For |
|-------|-------|---------|
| fatal | 60 | Process cannot continue |
| error | 50 | Failures requiring attention |
| warn | 40 | Recoverable issues |
| info | 30 | Business events, normal operations |
| debug | 20 | Detailed debugging information |
| trace | 10 | Very detailed tracing |
How It Works
The plugin registers two Fastify hooks:
onRequest— Creates a child Pino logger bound torequest.contextLogwith extracted context (requestId, orgId, userId, route, method, OpenTelemetry trace). Context is extracted fromrequest.user, standard headers (x-org-id,x-tenant-id,x-user-id), and thetraceparentheader. An optionalcontextExtractorfunction allows adding custom fields.onResponse— Logs every completed HTTP response with the canonicalhttp.responseevent, including status code, duration, and full request context. Log level is determined by status code:errorfor 5xx,warnfor 4xx,infootherwise.
The decorator methods (logEvent, logBoundary, createBoundaryLogger, createJobContext) are stateless utilities that format log entries and write them to the appropriate Pino logger instance. When a request parameter is provided, they merge in request context; otherwise they use fastify.log directly.
The exported getLoggerOptions() function is intended to be called at Fastify instantiation time to configure the Pino logger with redaction, serializers, base fields, and environment-aware transport selection (pino-pretty in development, JSON stdout in production, or a custom transport like @logtail/pino).
Testing
npm testLicense
UNLICENSED
