Koa Middleware
Integrate cenglu with Koa applications
Koa Middleware
Cenglu provides comprehensive middleware for Koa with automatic request logging, correlation ID tracking, and context propagation.
Installation
import Koa from "koa";
import { createLogger, koaMiddleware } from "cenglu";Basic Usage
import Koa from "koa";
import { createLogger, koaMiddleware } from "cenglu";
const app = new Koa();
const logger = createLogger({ service: "api" });
// Apply middleware
app.use(koaMiddleware(logger));
app.use(async (ctx) => {
// Logger is automatically attached with request context
ctx.logger.info("Processing request", { userId: ctx.query.userId });
ctx.body = { message: "Hello, World!" };
});
app.listen(3000);Automatic Features
The middleware automatically:
- ✅ Generates or extracts correlation IDs
- ✅ Logs incoming requests
- ✅ Logs outgoing responses with duration
- ✅ Attaches logger to
ctx.loggerandctx.state.logger - ✅ Attaches correlation ID to
ctx.correlationIdandctx.state.correlationId - ✅ Sets correlation ID response header
- ✅ Uses AsyncLocalStorage for context propagation
- ✅ Catches and logs errors
Configuration Options
app.use(koaMiddleware(logger, {
// 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 (requires koa-bodyparser)
includeResponseBody: false, // Include response body
maxResponseBodyLength: 1000,
// Correlation ID
correlationIdHeader: "x-correlation-id",
generateCorrelationId: () => crypto.randomUUID(),
// Path filtering
ignorePaths: ["/health", "/metrics", /^\/internal/],
skip: (ctx) => ctx.method === "OPTIONS",
// Security
redactHeaders: ["authorization", "cookie", "set-cookie"],
// Log levels
successLevel: "info", // 2xx, 3xx
clientErrorLevel: "warn", // 4xx
serverErrorLevel: "error", // 5xx
// Advanced
useAsyncContext: true,
// Custom messages
requestMessage: (ctx) => `${ctx.method} ${ctx.path}`,
responseMessage: (ctx, duration) =>
`${ctx.method} ${ctx.path} ${ctx.status} ${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" }
}
}With Headers
app.use(koaMiddleware(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
app.use(koaMiddleware(logger, {
successLevel: "info", // 200-399
clientErrorLevel: "warn", // 400-499
serverErrorLevel: "error", // 500-599
}));Status Code Mapping:
200-399→info(configurable withsuccessLevel)400-499→warn(configurable withclientErrorLevel)500-599→error(configurable withserverErrorLevel)
Correlation IDs
Automatic Generation
app.use(koaMiddleware(logger, {
correlationIdHeader: "x-correlation-id",
generateCorrelationId: () => crypto.randomUUID(),
}));
app.use(async (ctx) => {
console.log(ctx.correlationId); // "550e8400-e29b-41d4-a716-446655440000"
// All logs include correlation ID
ctx.logger.info("Processing request");
ctx.body = { users: [] };
});Extract from Incoming Headers
app.use(koaMiddleware(logger, {
correlationIdHeader: "x-correlation-id",
}));
// If client sends x-correlation-id or x-request-id header, it will be usedPropagate to Downstream Services
app.use(async (ctx) => {
const response = await fetch("https://api.example.com/users", {
headers: {
"x-correlation-id": ctx.correlationId, // Propagate
},
});
ctx.logger.info("Fetched users", {
count: response.data.length,
});
ctx.body = response.data;
});Path Filtering
Ignore Specific Paths
app.use(koaMiddleware(logger, {
ignorePaths: [
"/health", // Exact match
"/metrics", // Exact match
/^\/internal\//, // Regex pattern
],
}));Custom Skip Logic
app.use(koaMiddleware(logger, {
skip: (ctx) => {
// Skip OPTIONS requests
if (ctx.method === "OPTIONS") return true;
// Skip based on header
if (ctx.headers["x-skip-logging"]) return true;
return false;
},
}));Error Handling
Automatic Error Logging
The middleware catches errors and logs them automatically:
app.use(koaMiddleware(logger));
app.use(async (ctx) => {
throw new Error("Something went wrong");
// Error is automatically logged with full context
});Error Middleware
Use the error middleware for centralized error handling:
import { koaMiddleware, koaErrorMiddleware } from "cenglu";
// Error middleware FIRST
app.use(koaErrorMiddleware(logger, {
exposeErrors: process.env.NODE_ENV !== "production",
formatError: (err, ctx) => ({
error: {
message: err.message,
code: err.code,
},
correlationId: ctx.correlationId,
}),
emitError: true, // Emit 'error' event on app
}));
// Then logging middleware
app.use(koaMiddleware(logger));
// Your routes
app.use(async (ctx) => {
throw new Error("Something went wrong");
});Error Log Output
{
"level": "error",
"msg": "GET /users/123 500 12ms",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"err": {
"name": "Error",
"message": "Something went wrong",
"stack": "Error: Something went wrong\n at ..."
},
"context": {
"statusCode": 500,
"duration": 12
}
}AsyncLocalStorage Context
Enable context propagation across async operations:
app.use(koaMiddleware(logger, {
useAsyncContext: true, // Default: true
}));
app.use(async (ctx) => {
// Context is preserved across awaits
await someAsyncOperation();
// These logs still have request context
ctx.logger.info("Operation completed");
ctx.body = { success: true };
});
async function someAsyncOperation() {
// Can access context from anywhere in the call stack
const context = LoggerContext.get();
console.log(context?.correlationId);
}Body Parsing
To include request body in logs, use a body parser first:
import bodyParser from "koa-bodyparser";
// Body parser BEFORE logging middleware
app.use(bodyParser());
// Then logging middleware
app.use(koaMiddleware(logger, {
includeBody: true,
}));
app.use(async (ctx) => {
// Request body is now available and logged
ctx.logger.info("Creating user", { email: ctx.request.body.email });
ctx.body = { success: true };
});Custom Context
Add custom data to logs via ctx.state:
import jwt from "koa-jwt";
// Authentication middleware
app.use(jwt({ secret: process.env.JWT_SECRET }));
// Logging middleware
app.use(koaMiddleware(logger));
// Add user to state
app.use(async (ctx, next) => {
if (ctx.state.user) {
ctx.state.userId = ctx.state.user.id;
ctx.state.userEmail = ctx.state.user.email;
}
await next();
});
// Routes
app.use(async (ctx) => {
// Log with user context
ctx.logger.info("User action", {
userId: ctx.state.userId,
action: "view_profile",
});
ctx.body = { user: ctx.state.user };
});TypeScript Support
Add type augmentation for TypeScript:
import type { Logger } from "cenglu";
declare module "koa" {
interface BaseContext {
logger: Logger;
correlationId: string;
}
interface DefaultState {
logger: Logger;
correlationId: string;
}
}Complete Example
import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { createLogger, koaMiddleware, koaErrorMiddleware } from "cenglu";
const logger = createLogger({
service: "user-api",
env: process.env.NODE_ENV,
level: "info",
redaction: { enabled: true },
});
const app = new Koa();
const router = new Router();
// Error middleware (first)
app.use(koaErrorMiddleware(logger, {
exposeErrors: process.env.NODE_ENV !== "production",
}));
// Body parser
app.use(bodyParser());
// Logging middleware
app.use(koaMiddleware(logger, {
logRequests: true,
logResponses: true,
includeQuery: true,
ignorePaths: ["/health", "/metrics"],
}));
// Health check
router.get("/health", (ctx) => {
ctx.body = { status: "ok" };
});
// Routes
router.get("/users/:id", async (ctx) => {
const timer = ctx.logger.time("fetch-user");
try {
const user = await fetchUser(ctx.params.id);
timer.endWithContext({ userId: user.id });
ctx.logger.info("User fetched successfully", { userId: user.id });
ctx.body = user;
} catch (error) {
ctx.logger.error("Failed to fetch user", error, {
userId: ctx.params.id,
});
ctx.status = 404;
ctx.body = { error: "User not found" };
}
});
router.post("/users", async (ctx) => {
const body = ctx.request.body as { email: string; name: string };
ctx.logger.info("Creating user", { email: body.email });
try {
const user = await createUser(body);
ctx.logger.info("User created", { userId: user.id });
ctx.status = 201;
ctx.body = user;
} catch (error) {
ctx.logger.error("Failed to create user", error);
ctx.status = 500;
ctx.body = { error: "Failed to create user" };
}
});
// Register routes
app.use(router.routes());
app.use(router.allowedMethods());
// Error event handler
app.on("error", (err, ctx) => {
logger.error("Application error", err, {
path: ctx.path,
method: ctx.method,
});
});
// Graceful shutdown
const closeGracefully = async (signal: string) => {
logger.info(`Received ${signal}, shutting down gracefully`);
// Close server
server.close(async () => {
await logger.flush();
await logger.close();
logger.info("Application stopped");
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
logger.error("Forced shutdown after timeout");
process.exit(1);
}, 10000);
};
process.on("SIGTERM", () => closeGracefully("SIGTERM"));
process.on("SIGINT", () => closeGracefully("SIGINT"));
// Start server
const server = app.listen(3000, () => {
logger.info("Server started", {
port: 3000,
nodeVersion: process.version,
});
});
// Handle startup errors
server.on("error", (error) => {
logger.fatal("Failed to start server", error);
process.exit(1);
});Middleware Composition
Combine with other Koa middleware:
import jwt from "koa-jwt";
import cors from "@koa/cors";
import compress from "koa-compress";
import ratelimit from "koa-ratelimit";
// Error handler (first)
app.use(koaErrorMiddleware(logger));
// CORS
app.use(cors());
// Compression
app.use(compress());
// Rate limiting
app.use(ratelimit({
driver: "memory",
db: new Map(),
duration: 60000,
errorMessage: "Too many requests",
id: (ctx) => ctx.ip,
max: 100,
}));
// Body parser
app.use(bodyParser());
// Logging (before authentication)
app.use(koaMiddleware(logger));
// Authentication
app.use(jwt({ secret: process.env.JWT_SECRET }));
// Routes
app.use(router.routes());Testing
Mock the logger in tests:
import request from "supertest";
import { createLogger } from "cenglu";
describe("API Tests", () => {
it("should log requests", async () => {
const logs: any[] = [];
const testLogger = createLogger({
adapters: [
{
name: "test",
handle: (record) => logs.push(record),
},
],
});
const app = createApp(testLogger);
await request(app.callback())
.get("/users")
.expect(200);
expect(logs).toContainEqual(
expect.objectContaining({
msg: expect.stringContaining("GET /users"),
level: "info",
})
);
});
});Performance Considerations
The middleware 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
}),
],
});
app.use(koaMiddleware(logger, {
includeBody: false, // Disable body logging
includeHeaders: false, // Disable header logging
includeResponseBody: false, // Disable response body logging
}));