@xenterprises/fastify-xlogger
v1.1.2
Published
Fastify plugin for standardized logging with Pino - context, redaction, and canonical schema
Downloads
41
Readme
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.
Philosophy
Logging is infrastructure. Centralize it and keep it boring.
- Use Fastify's built-in Pino logger as the single logging engine
- Never create a second logger
- Log structured objects, not concatenated strings
- Automatically redact secrets at the logger level
- Standardize context across all log entries
Installation
npm install xloggerQuick Start
import Fastify from "fastify";
import xLogger, { getLoggerOptions } from "xlogger";
// Create Fastify with xLogger-configured Pino options
const fastify = Fastify({
logger: getLoggerOptions({
serviceName: "my-api",
}),
});
// Register the plugin
await fastify.register(xLogger, {
serviceName: "my-api",
});
// Use structured logging everywhere
fastify.get("/users/:id", async (request, reply) => {
const { id } = request.params;
// Use the context-aware logger
request.contextLog.info({ userId: id }, "Fetching user");
// Or use the standard logger with context extraction
fastify.xlogger.logEvent({
event: "user.fetched",
data: { userId: id },
request,
});
return { id };
});Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| active | boolean | true | Enable/disable the plugin |
| serviceName | string | process.env.SERVICE_NAME | Service name for logs |
| environment | string | process.env.NODE_ENV | Environment name |
| redactPaths | string[] | [] | Additional paths to redact |
| redactClobber | boolean | false | Replace default redact paths |
| contextExtractor | function | null | Custom context extraction function |
| enableBoundaryLogging | boolean | true | Enable boundary logging helpers |
Features
1. Automatic Request Context
Every log entry automatically includes:
requestId- Unique request identifierorgId- Tenant/organization ID (from headers or user object)userId- User ID (from headers or user object)route- Route patternmethod- HTTP methodtraceId/spanId- OpenTelemetry context (if present)
// Context is automatically added to request.contextLog
request.contextLog.info({ action: "invite_sent" }, "Invite sent");
// Output includes requestId, orgId, userId, route, method automatically2. Secret Redaction
Secrets are automatically redacted at the logger level:
// These fields are automatically redacted:
// - authorization, cookie, set-cookie headers
// - password, token, secret, apiKey fields
// - cardNumber, cvv, ssn, creditCard
// - Nested paths like *.password, *.token
fastify.log.info({
user: {
email: "[email protected]",
password: "secret123" // Will be logged as [REDACTED]
}
}, "User data");Default Redact Paths:
req.headers.authorizationreq.headers.cookiereq.headers['set-cookie']req.headers['x-api-key']password,token,secret,apiKey,api_keyaccessToken,refreshToken,privateKeycardNumber,cvv,ssn,creditCard*.password,*.token,*.secret,*.apiKey
3. Canonical Log Schema
Recommended fields for consistent log structure:
| Field | Description |
|-------|-------------|
| event | Event name (e.g., "user.created", "payment.completed") |
| msg | Human-readable message |
| requestId | Request identifier |
| orgId | Organization/tenant ID |
| userId | User ID |
| route | Route pattern |
| method | HTTP method |
| statusCode | Response status code |
| durationMs | Duration in milliseconds |
| err | Error object |
| vendor | External service name |
| externalId | External resource ID |
4. Boundary Logging (External API Calls)
Log external API calls with timing, vendor IDs, and retry info:
// Simple boundary logging
fastify.xlogger.logBoundary({
vendor: "stripe",
operation: "createCustomer",
externalId: "cus_123",
durationMs: 150,
success: true,
request,
});
// Or use the boundary logger helper with automatic timing
const boundary = fastify.xlogger.createBoundaryLogger("stripe", "createCustomer", request);
try {
const customer = await stripe.customers.create({ email });
boundary.success({ externalId: customer.id, statusCode: 200 });
} catch (err) {
boundary.fail(err, { statusCode: err.statusCode });
}
// With retries
const boundary = fastify.xlogger.createBoundaryLogger("twilio", "sendSMS", request);
for (let i = 0; i < 3; i++) {
try {
const result = await twilio.messages.create({ to, body });
boundary.success({ externalId: result.sid });
break;
} catch (err) {
boundary.retry();
if (i === 2) boundary.fail(err);
}
}5. Background Job Correlation
Pass context to background jobs for tracing:
// In your route handler
fastify.post("/process", async (request, reply) => {
const context = fastify.xlogger.extractContext(request);
// Queue the job with context
await queue.add("processData", {
data: request.body,
...context,
});
return { queued: true };
});
// In your job processor
async function processJob(job) {
const { requestId, orgId, userId, data } = job.data;
const jobContext = fastify.xlogger.createJobContext({
jobName: "processData",
requestId,
orgId,
userId,
});
jobContext.start({ itemCount: data.length });
try {
await processData(data);
jobContext.complete({ processed: data.length });
} catch (err) {
jobContext.fail(err);
throw err;
}
}6. Environment-Aware Output
- Production: JSON logs for aggregation systems
- Development: Pretty-printed logs with colors
// Automatically detected from NODE_ENV
const options = getLoggerOptions();
// Or force pretty printing
const options = getLoggerOptions({ pretty: true });7. Custom Transports (Betterstack, Logtail, etc.)
Send logs to centralized logging services like Betterstack/Logtail using custom transports:
Install Transport (Optional Peer Dependency)
npm install @logtail/pinoSingle Transport (Betterstack)
import Fastify from "fastify";
import xLogger, { getLoggerOptions } from "xlogger";
const fastify = Fastify({
logger: getLoggerOptions({
serviceName: "my-api",
transport: {
target: "@logtail/pino",
options: {
sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
},
},
}),
});
await fastify.register(xLogger, {
serviceName: "my-api",
});Multiple Transports
Send logs to both Betterstack and a local file:
const fastify = Fastify({
logger: getLoggerOptions({
serviceName: "my-api",
transport: {
targets: [
{
target: "@logtail/pino",
options: {
sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
},
},
{
target: "pino/file",
options: {
destination: "/var/log/app.log",
},
},
],
},
}),
});Environment-Based Configuration
// In development: use pino-pretty (default)
// In production: use Betterstack if token is set, otherwise JSON stdout
const transport = process.env.BETTERSTACK_SOURCE_TOKEN
? {
target: "@logtail/pino",
options: {
sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
},
}
: undefined;
const fastify = Fastify({
logger: getLoggerOptions({
serviceName: "my-api",
transport,
}),
});Note: When a custom transport is provided, it overrides the default environment-based transport configuration (pino-pretty in development, JSON in production).
API Reference
Decorators
| Decorator | Description |
|-----------|-------------|
| fastify.xlogger.config | Plugin configuration |
| fastify.xlogger.extractContext(request) | Extract context from request |
| fastify.xlogger.logEvent(params) | Log a business event |
| fastify.xlogger.logBoundary(params) | Log an external API call |
| fastify.xlogger.createBoundaryLogger(vendor, op, req) | Create timed boundary logger |
| fastify.xlogger.createJobContext(params) | Create background job context |
| fastify.xlogger.levels | Log level constants |
| fastify.xlogger.redactPaths | Configured redact paths |
| request.contextLog | Child logger with request context |
logEvent(params)
Log a business event with canonical schema.
fastify.xlogger.logEvent({
event: "user.created", // Required: event name
msg: "User was created", // Optional: message
level: "info", // Optional: log level (default: "info")
data: { email: "[email protected]" }, // Optional: additional data
request, // Optional: request for context
});logBoundary(params)
Log an external API call.
fastify.xlogger.logBoundary({
vendor: "stripe", // Required: service name
operation: "createCustomer", // Required: operation name
externalId: "cus_123", // Optional: external resource ID
durationMs: 150, // Optional: call duration
statusCode: 200, // Optional: response status
success: true, // Optional: success flag (default: true)
retryCount: 0, // Optional: number of retries
metadata: {}, // Optional: additional metadata
err: null, // Optional: error if failed
request, // Optional: request for context
});createBoundaryLogger(vendor, operation, request)
Create a boundary logger with automatic timing.
const boundary = fastify.xlogger.createBoundaryLogger("stripe", "createCustomer", request);
boundary.retry(); // Increment retry counter
boundary.success(params); // Log success with timing
boundary.fail(err, params); // Log failure with timingcreateJobContext(params)
Create a correlation context for background jobs.
const job = fastify.xlogger.createJobContext({
jobName: "processPayments", // Required: job name
requestId: "req_123", // Optional: original request ID
orgId: "org_456", // Optional: organization ID
userId: "user_789", // Optional: user ID
correlationId: "corr_abc", // Optional: correlation ID (auto-generated if not provided)
});
job.context; // { correlationId, jobName, orgId, userId, originalRequestId }
job.log; // Child logger with context
job.start(data); // Log job started
job.complete(data); // Log job completed
job.fail(err, data); // Log job failedgetLoggerOptions(options)
Get Pino logger options for Fastify initialization.
import { getLoggerOptions } from "xlogger";
const fastify = Fastify({
logger: getLoggerOptions({
level: "debug", // Optional: log level
serviceName: "my-api", // Optional: service name
redactPaths: ["custom"], // Optional: additional redact paths
pretty: false, // Optional: force pretty printing
transport: { // Optional: custom transport configuration
target: "@logtail/pino",
options: {
sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
},
},
}),
});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 |
Best Practices
Do
// Log structured objects
fastify.log.info({ userId, action: "login" }, "User logged in");
// Use the canonical schema
fastify.xlogger.logEvent({
event: "payment.completed",
data: { amount: 100, currency: "USD" },
request,
});
// Use boundary logging for external calls
const boundary = fastify.xlogger.createBoundaryLogger("stripe", "charge", request);Don't
// Don't concatenate strings
fastify.log.info("User " + userId + " logged in"); // Bad!
// Don't log sensitive data (it will be redacted, but still)
fastify.log.info({ password: userPassword }); // Bad!
// Don't log full request/response payloads
fastify.log.info({ body: request.body }); // Usually bad!Integration with Error Tracking
Use logs for aggregation/search and Sentry for error tracking:
fastify.setErrorHandler((error, request, reply) => {
// Log the error
request.contextLog.error({ err: error }, "Request failed");
// Send to Sentry with context
Sentry.captureException(error, {
extra: fastify.xlogger.extractContext(request),
});
reply.status(500).send({ error: "Internal Server Error" });
});Testing
npm testLicense
MIT
