Like Blockend? Give it a ⭐ on GitHub.

Star
blockend

Blockend

02 blocks

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, AsyncLocalStorage context, and runWithLoggerContext.

Installation

pnpm dlx blockend-cli add logger

Detect 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

PackageRequired for
pinoCore logger
pino-prettyDev pretty-printing
expressExpress adapter
fastifyFastify adapter
honoHono 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

ExportTypeDescription
loggerpino.LoggerShared logger instance used throughout your application
loggerContextAsyncLocalStorage<LoggerContext>The underlying AsyncLocalStorage instance
runWithLoggerContext()<T>(incomingId, callback) => TCreates a logger context with a request ID
getRequestId()() => string | undefinedReturns the current request ID or undefined outside a request context
expressLoggerAdapter()(req, res, next) => voidExpress middleware
fastifyLoggerFastifyPluginAsyncFastify plugin
honoLoggerAdapter()() => MiddlewareHandlerHono 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(); // undefined

Background 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());
});

  • 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.

On this page