Fastify Plugin
Integrate cenglu with Fastify applications
Fastify Plugin
Cenglu provides a native Fastify plugin for seamless request logging, correlation ID tracking, and context propagation.
Installation
import Fastify from "fastify";
import { createLogger, fastifyPlugin } from "cenglu";Basic Usage
import Fastify from "fastify";
import { createLogger, fastifyPlugin } from "cenglu";
const fastify = Fastify();
const logger = createLogger({ service: "api" });
// Register plugin
await fastify.register(fastifyPlugin, {
logger,
ignorePaths: ["/health"],
});
fastify.get("/users/:id", async (request, reply) => {
// Logger is automatically attached with request context
request.logger.info("Fetching user", { userId: request.params.id });
return { id: request.params.id, name: "John Doe" };
});
await fastify.listen({ port: 3000 });Automatic Features
The plugin automatically:
- ✅ Generates or extracts correlation IDs
- ✅ Logs incoming requests
- ✅ Logs outgoing responses with duration
- ✅ Attaches logger to
request.logger - ✅ Attaches correlation ID to
request.correlationId - ✅ Sets correlation ID response header
- ✅ Uses AsyncLocalStorage for context propagation
- ✅ Handles errors with proper logging
Configuration Options
await fastify.register(fastifyPlugin, {
// Required: Logger instance
logger: createLogger({ service: "api" }),
// Logging control
logRequests: true, // Log incoming requests
logResponses: true, // Log outgoing responses
// Data inclusion
includeHeaders: false, // Include request headers (may contain sensitive data)
includeQuery: true, // Include query parameters
includeBody: false, // Include request body
// Correlation ID
correlationIdHeader: "x-correlation-id",
generateCorrelationId: () => crypto.randomUUID(),
// Path filtering
ignorePaths: ["/health", "/metrics", /^\/internal/],
skip: (request, reply) => request.method === "OPTIONS",
// Security
redactHeaders: ["authorization", "cookie", "set-cookie"],
// Advanced
useAsyncContext: true,
// Custom messages
requestMessage: (request) => `${request.method} ${request.routerPath}`,
responseMessage: (request, reply, duration) =>
`${request.method} ${request.routerPath} ${reply.statusCode} ${duration}ms`,
});Request Logging
Default Request Log
{
"level": "info",
"msg": "GET /users/123",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/users/123",
"context": {
"url": "/users/123?includeDetails=true",
"query": { "includeDetails": "true" },
"params": { "id": "123" }
}
}With Headers
await fastify.register(fastifyPlugin, {
logger,
includeHeaders: true,
redactHeaders: ["authorization", "cookie"],
});Response Logging
Default Response Log
{
"level": "info",
"msg": "GET /users/123 200 42ms",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/users/123",
"context": {
"statusCode": 200,
"duration": 42
}
}Log Levels by Status Code
200-399→info400-499→warn500-599→error
Correlation IDs
Automatic Generation
await fastify.register(fastifyPlugin, {
logger,
correlationIdHeader: "x-correlation-id",
generateCorrelationId: () => crypto.randomUUID(),
});
fastify.get("/users", async (request, reply) => {
console.log(request.correlationId); // "550e8400-e29b-41d4-a716-446655440000"
// All logs include correlation ID
request.logger.info("Processing request");
return { users: [] };
});Extract from Request ID
Fastify's built-in request.id is used as fallback:
await fastify.register(fastifyPlugin, {
logger,
});
// If no x-correlation-id header, falls back to request.idPropagate to Downstream Services
fastify.get("/users", async (request, reply) => {
const response = await fetch("https://api.example.com/users", {
headers: {
"x-correlation-id": request.correlationId, // Propagate
},
});
request.logger.info("Fetched users", {
count: response.data.length,
});
return response.data;
});Path Filtering
Ignore Specific Paths
await fastify.register(fastifyPlugin, {
logger,
ignorePaths: [
"/health", // Exact match
"/metrics", // Exact match
/^\/internal\//, // Regex pattern
],
});Custom Skip Logic
await fastify.register(fastifyPlugin, {
logger,
skip: (request, reply) => {
// Skip OPTIONS requests
if (request.method === "OPTIONS") return true;
// Skip based on header
if (request.headers["x-skip-logging"]) return true;
return false;
},
});Error Handling
The plugin automatically hooks into Fastify's error handling:
fastify.get("/error", async (request, reply) => {
throw new Error("Something went wrong");
});
// Error is automatically logged with:
// - Error message and stack trace
// - Status code
// - Request contextCustom Error Handler
fastify.setErrorHandler((error, request, reply) => {
// request.logger is available here
request.logger.error("Custom error handler", error, {
customData: "value",
});
reply.status(500).send({
error: error.message,
correlationId: request.correlationId,
});
});Hooks Integration
The plugin uses Fastify hooks:
- onRequest: Sets up logger, correlation ID, and logs request
- onResponse: Logs response with duration
- onError: Logs errors
You can add additional hooks:
fastify.addHook("preHandler", async (request, reply) => {
// request.logger is available
request.logger.debug("Pre-handler hook", {
user: request.user?.id,
});
});AsyncLocalStorage Context
Enable context propagation across async operations:
await fastify.register(fastifyPlugin, {
logger,
useAsyncContext: true, // Default: true
});
fastify.get("/users", async (request, reply) => {
// Context is preserved across awaits
await someAsyncOperation();
// These logs still have request context
request.logger.info("Operation completed");
return { users: [] };
});
async function someAsyncOperation() {
// Can access context from anywhere in the call stack
const context = LoggerContext.get();
console.log(context?.correlationId);
}TypeScript Support
Add type augmentation for TypeScript:
import type { Logger } from "cenglu";
declare module "fastify" {
interface FastifyRequest {
logger: Logger;
correlationId: string;
}
}Complete Example
import Fastify from "fastify";
import { createLogger, fastifyPlugin } from "cenglu";
const logger = createLogger({
service: "user-api",
env: process.env.NODE_ENV,
level: "info",
redaction: { enabled: true },
});
const fastify = Fastify({
logger: false, // Disable default Fastify logger
requestIdHeader: "x-request-id",
genReqId: () => crypto.randomUUID(),
});
// Register cenglu plugin
await fastify.register(fastifyPlugin, {
logger,
logRequests: true,
logResponses: true,
includeQuery: true,
ignorePaths: ["/health", "/metrics"],
});
// Health check
fastify.get("/health", async (request, reply) => {
return { status: "ok" };
});
// Routes
fastify.get("/users/:id", async (request, reply) => {
const timer = request.logger.time("fetch-user");
try {
const user = await fetchUser(request.params.id);
timer.endWithContext({ userId: user.id });
request.logger.info("User fetched successfully", { userId: user.id });
return user;
} catch (error) {
request.logger.error("Failed to fetch user", error, {
userId: request.params.id,
});
reply.code(404);
return { error: "User not found" };
}
});
fastify.post("/users", async (request, reply) => {
const body = request.body as { email: string; name: string };
request.logger.info("Creating user", { email: body.email });
try {
const user = await createUser(body);
request.logger.info("User created", { userId: user.id });
reply.code(201);
return user;
} catch (error) {
request.logger.error("Failed to create user", error);
reply.code(500);
return { error: "Failed to create user" };
}
});
// Error handler
fastify.setErrorHandler((error, request, reply) => {
request.logger.error("Unhandled error", error);
reply.status(500).send({
error: "Internal Server Error",
correlationId: request.correlationId,
});
});
// Graceful shutdown
const closeGracefully = async (signal: string) => {
logger.info(`Received ${signal}, shutting down gracefully`);
await fastify.close();
await logger.flush();
await logger.close();
logger.info("Application stopped");
process.exit(0);
};
process.on("SIGTERM", () => closeGracefully("SIGTERM"));
process.on("SIGINT", () => closeGracefully("SIGINT"));
// Start server
try {
await fastify.listen({ port: 3000, host: "0.0.0.0" });
logger.info("Server started", {
port: 3000,
nodeVersion: process.version,
});
} catch (error) {
logger.fatal("Failed to start server", error);
process.exit(1);
}Plugin Composition
Combine with other Fastify plugins:
import fastifyAuth from "@fastify/auth";
import fastifyJwt from "@fastify/jwt";
// Register cenglu first
await fastify.register(fastifyPlugin, { logger });
// Then register other plugins
await fastify.register(fastifyJwt, { secret: process.env.JWT_SECRET });
await fastify.register(fastifyAuth);
// Use in routes
fastify.route({
method: "GET",
url: "/protected",
preHandler: fastify.auth([fastify.verifyJWT]),
handler: async (request, reply) => {
request.logger.info("Accessing protected route", {
userId: request.user.id,
});
return { message: "Protected data" };
},
});Testing
Mock the logger in tests:
import { test } from "tap";
import { build } from "./app"; // Your app builder
test("GET /users returns users", async (t) => {
const logs: any[] = [];
const app = await build({
logger: createLogger({
adapters: [
{
name: "test",
handle: (record) => logs.push(record),
},
],
}),
});
const response = await app.inject({
method: "GET",
url: "/users",
});
t.equal(response.statusCode, 200);
t.ok(logs.some((log) => log.msg.includes("GET /users")));
await app.close();
});Performance Considerations
The plugin has minimal overhead:
- Logger attachment: O(1) operation
- Correlation ID: Single UUID generation
- Request logging: ~0.1ms average
- Response logging: ~0.1ms average
For high-throughput services:
import { samplingPlugin } from "cenglu";
const logger = createLogger({
plugins: [
samplingPlugin({
rates: { debug: 0.1 }, // Sample 10% of debug logs
}),
],
});
await fastify.register(fastifyPlugin, {
logger,
includeBody: false, // Disable body logging
includeHeaders: false, // Disable header logging
});