Logger
Structured, request-aware logging with automatic request IDs and framework adapters for Express, Fastify, and Hono.
The Logger block provides a shared, structured logger for your application with automatic request context propagation using Node.js AsyncLocalStorage.
Every log created during a request automatically includes the same request ID, making it easy to trace a request across your application without manually passing IDs between functions.
Features
- Shared logger instance for your entire application
- Automatic request ID generation and propagation
- Request context powered by
AsyncLocalStorage - Automatic HTTP request logging
- Express, Fastify, and Hono adapters
- Built-in redaction for common sensitive fields
- Pretty logs during development
- JSON logs in production
- TypeScript support
File Structure
logger
├── adapters
│ ├── express.ts
│ ├── fastify.ts
│ └── hono.ts
├── core.ts
└── core.test.ts- adapters/ — Framework-specific middleware for Express, Fastify, and Hono that initialize request context and log completed requests.
- core.ts — Core logger setup with Pino,
AsyncLocalStoragecontext, andrunWithLoggerContext.
Installation
pnpm dlx blockend-cli add loggerDetect Project
Blockend detects your project's framework, language, aliases and configuration.
Install Dependencies
The required dependencies are installed automatically.
Generate Files
The logger block is generated inside your configured blocks directory.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Package | Required for |
|---|---|
pino | Core logger |
pino-pretty | Dev pretty-printing |
express | Express adapter |
fastify | Fastify adapter |
hono | Hono adapter |
blocks/logger/core.ts
Core logger setup with Pino, AsyncLocalStorage context, and utility functions.
import { pino } from "pino";
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
export interface LoggerContext {
requestId: string;
[key: string]: unknown;
}
export const loggerContext = new AsyncLocalStorage<LoggerContext>();
export function getRequestId(): string | undefined {
return loggerContext.getStore()?.requestId;
}
const isProduction = process.env.NODE_ENV === "production";
export const logger = pino({
level: isProduction ? "info" : "debug",
mixin() {
const store = loggerContext.getStore();
return store ? { requestId: store.requestId } : {};
},
redact: {
paths: ["Authorization", "*.token", "token", "*.password", "password"],
censor: "[REDACTED]"
},
...(isProduction
? {}
: {
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard",
ignore: "hostname,pid"
}
}
})
});
export function runWithLoggerContext<T>(
incomingId: string | undefined | null,
callback: (requestId: string) => T
) {
const requestId = incomingId || randomUUID();
return loggerContext.run({ requestId }, () => callback(requestId));
}
blocks/logger/adapters/express.ts
Express middleware that initializes request context and logs completed requests.
import type { Request, Response, NextFunction } from "express";
import { logger, runWithLoggerContext } from "../core.js";
interface LoggedRequest extends Request {
id?: string;
}
export function expressLoggerAdapter(req: LoggedRequest, res: Response, next: NextFunction): void {
const rawId = req.headers["x-request-id"]?.toString();
runWithLoggerContext(rawId, (requestId) => {
req.id = requestId;
const start = performance.now();
res.on("finish", () => {
logger.info(
{
http: {
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs: Math.round(performance.now() - start)
}
},
`HTTP ${req.method} ${req.path} completed`
);
});
next();
});
}
blocks/logger/adapters/fastify.ts
Fastify plugin that initializes request context and logs completed requests.
import type { FastifyPluginAsync } from "fastify";
import fp from "fastify-plugin";
import { runWithLoggerContext, loggerContext, logger } from "../core.js";
// Augment Fastify types to natively recognize custom request properties without 'any' casting
declare module "fastify" {
interface FastifyRequest {
requestId: string;
}
}
declare module "http" {
interface IncomingMessage {
_startTime?: number;
}
}
const fastifyLoggerPlugin: FastifyPluginAsync = async (fastify) => {
// Hook 1: Capture incoming request metadata and establish ALS context
fastify.addHook("onRequest", (request, reply, done) => {
const rawId = request.headers["x-request-id"]?.toString();
request.raw._startTime = performance.now();
runWithLoggerContext(rawId, (requestId) => {
request.requestId = requestId;
// Keep the response finish listener tied directly to the execution scope
reply.raw.on("finish", () => {
const start = request.raw._startTime || performance.now();
logger.info(
{
http: {
method: request.method,
path: request.url,
statusCode: reply.statusCode,
durationMs: Math.round(performance.now() - start)
}
},
`HTTP ${request.method} ${request.url} completed`
);
});
done();
});
});
// Hook 2: Re-bind right before running the route handler to shield the context
fastify.addHook("preHandler", (request, _reply, done) => {
const currentId = request.requestId;
if (currentId) {
loggerContext.run({ requestId: currentId }, () => {
done();
});
} else {
done();
}
});
};
export const fastifyLogger = fp(fastifyLoggerPlugin);
blocks/logger/adapters/hono.ts
Hono middleware that initializes request context and logs completed requests.
import type { MiddlewareHandler } from "hono";
import { logger, runWithLoggerContext } from "../core.js";
export function honoLoggerAdapter(): MiddlewareHandler {
return async (c, next) => {
const rawId = c.req.header("x-request-id");
const start = performance.now();
// Wrap the next execution flow in your core ALS context
await runWithLoggerContext(rawId, async (requestId) => {
// Attach the generated/incoming ID to Hono's execution context variables
c.set("requestId", requestId);
// Continue processing the request chain
await next();
// Log response details after execution has completed
logger.info(
{
http: {
method: c.req.method,
path: c.req.path,
statusCode: c.res.status,
durationMs: Math.round(performance.now() - start)
}
},
`HTTP ${c.req.method} ${c.req.path} completed`
);
});
};
}
Configuration
| Export | Type | Description |
|---|---|---|
logger | pino.Logger | Shared logger instance used throughout your application |
loggerContext | AsyncLocalStorage<LoggerContext> | The underlying AsyncLocalStorage instance |
runWithLoggerContext() | <T>(incomingId, callback) => T | Creates a logger context with a request ID |
getRequestId() | () => string | undefined | Returns the current request ID or undefined outside a request context |
expressLoggerAdapter() | (req, res, next) => void | Express middleware |
fastifyLogger | FastifyPluginAsync | Fastify plugin |
honoLoggerAdapter() | () => MiddlewareHandler | Hono middleware |
Architecture
Request
│
▼
Adapter (Express / Fastify / Hono)
│ (extracts x-request-id, calls runWithLoggerContext)
▼
AsyncLocalStorage context
│ (stores requestId)
▼
Route Handler
│ (logger.info / logger.warn / logger.error — auto-includes requestId)
▼
Response
│
▼
Adapter logs HTTP completion
│ (method, path, statusCode, durationMs)The adapter extracts an optional x-request-id header, generates a UUID if none exists, and runs the request inside an AsyncLocalStorage context. Every log call during the request automatically includes the requestId. When the response finishes, the adapter logs the HTTP method, path, status code, and duration.
When to Use
- You need structured, JSON logging across your application.
- You want automatic request tracing without manually passing request IDs.
- You use multiple frameworks and want a consistent logging approach.
When Not to Use
- You prefer a different logging library (Winston, etc.).
- You don't need request-scoped logging.
- You're building a CLI tool or background worker without HTTP context.
Usage
Express
import express from "express";
import { expressLoggerAdapter, logger } from "@/blocks/logger/core";
const app = express();
app.use(expressLoggerAdapter);
app.get("/", (req, res) => {
logger.info("Handling request"); // auto-includes requestId
res.json({ ok: true });
});
app.listen(3000);Fastify
import Fastify from "fastify";
import { fastifyLogger, logger } from "@/blocks/logger/core";
const app = Fastify({ logger: false }); // use our logger instead
await app.register(fastifyLogger);
app.get("/", async () => {
logger.info("Handling request");
return { ok: true };
});
await app.listen({ port: 3000 });Hono
import { Hono } from "hono";
import { honoLoggerAdapter, logger } from "@/blocks/logger/core";
const app = new Hono();
app.use("*", honoLoggerAdapter());
app.get("/", (c) => {
logger.info("Handling request");
return c.text("ok");
});
export default app;Accessing the Request ID
import { getRequestId } from "@/blocks/logger/core";
// Inside a request context — returns the current requestId
const id = getRequestId();
// Outside a request context — returns undefined
const id = getRequestId(); // undefinedBackground Jobs
The AsyncLocalStorage context only exists during HTTP requests. For background jobs, create a context manually:
import { runWithLoggerContext, logger } from "@/blocks/logger/core";
await runWithLoggerContext(null, (requestId) => {
logger.info({ jobId: "abc" }, "Processing job"); // requestId is auto-generated
});API Reference
logger
const logger: pino.Logger;Shared Pino logger instance. Use throughout your application for structured logging.
runWithLoggerContext
function runWithLoggerContext<T>(
incomingId: string | undefined | null,
callback: (requestId: string) => T
): T;Creates an AsyncLocalStorage context with a request ID. If incomingId is null or undefined, a UUID is generated automatically.
getRequestId
function getRequestId(): string | undefined;Returns the current request ID from the active AsyncLocalStorage context, or undefined if no context exists.
expressLoggerAdapter
function expressLoggerAdapter(req: Request, res: Response, next: NextFunction): void;Express middleware. Extracts x-request-id header, initializes context, and logs HTTP completion on response finish.
fastifyLogger
const fastifyLogger: FastifyPluginAsync;Fastify plugin. Registers onRequest and preHandler hooks to initialize context and log HTTP completion.
honoLoggerAdapter
function honoLoggerAdapter(): MiddlewareHandler;Hono middleware. Initializes context, sets requestId in Hono's context variables, and logs HTTP completion.
LoggerContext
interface LoggerContext {
requestId: string;
[key: string]: unknown;
}Examples
Redacted Fields
Sensitive fields are automatically redacted in log output:
logger.info(
{
password: "super-secret",
Authorization: "Bearer xyz",
nested: { token: "jwt" }
},
"Sensitive operation"
);
// password, Authorization, and nested.token are redacted to "[REDACTED]"Custom Request ID
Provide your own request ID (e.g., from an API gateway):
app.use((req, res, next) => {
const requestId = req.headers["x-request-id"] ?? crypto.randomUUID();
runWithLoggerContext(requestId, () => next());
});Related Blocks
- Error Handler — Log unhandled errors with structured request context instead of
console.error.
FAQ
How does the request ID propagate?
The adapter wraps the request handler in runWithLoggerContext, which uses Node.js AsyncLocalStorage. The logger instance's mixin function reads the store and includes the requestId in every log line.
What headers are checked for an incoming request ID?
The x-request-id header. If present, it is reused. If absent, a UUID is generated.
Does this work with background jobs?
No. The AsyncLocalStorage context only exists during an HTTP request. For background jobs, create a logger context manually using runWithLoggerContext.