npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pino-ctx

v1.0.0

Published

AsyncLocalStorage-powered contextual logging for Pino

Readme

pino-ctx

Zero-boilerplate request context for Pino. Set it once in your middleware — every log across every async call carries it automatically.

npm install pino-ctx pino

npm version npm downloads Node.js >= 18 License: MIT


The Problem

You're three function calls deep. You need requestId in your logs. So you either pass logger through every function signature — or your logs come out naked.

// 😩 the "logger as a hot potato" pattern
async function handleRequest(req, res) {
  const logger = pino.child({ requestId: req.id, userId: req.user.id })
  await processOrder(logger, req.body)           // pass it
}

async function processOrder(logger, order) {
  await validateInventory(logger, order)         // pass it again
}

async function validateInventory(logger, order) {
  await reserveStock(logger, order)              // and again
}

And once you hit Promise.all, you've lost track of which log belongs to which request.

The Fix

// 😌 set once, available everywhere
app.use(createExpressMiddleware())

async function reserveStock(order) {
  logger.info('reserving stock')
  // → {"msg":"reserving stock","requestId":"abc-123","method":"POST","path":"/orders","traceId":"4bf92f3..."}
}

pino-ctx uses Node.js's built-in AsyncLocalStorage to bind context to the current async execution chain — no prop drilling, no global mutation, no context bleed between concurrent requests.


Quick Start

// logger.ts
import { createContextLogger, createContextStore } from 'pino-ctx'

export const store = createContextStore()

export const { logger, setContext, getContext, withContext } = createContextLogger({
  level: 'info',
  store,
})
// server.ts
import express from 'express'
import { createExpressMiddleware } from 'pino-ctx'
import { logger, store } from './logger'

const app = express()

app.use(createExpressMiddleware({ store }))   // ← logger and middleware share the same store

app.get('/orders', async (req, res) => {
  logger.info('fetching orders')
  const orders = await getOrders()  // logs inside here get the same context
  res.json(orders)
})

Every log in every function called from this request — no matter how deeply nested — will include requestId, method, path, and any W3C trace headers automatically.


Features

  • Automatic context propagation via AsyncLocalStorage — survives Promise.all, setTimeout, EventEmitter, everything
  • Zero-config adapters for Express, Fastify, Koa, and Hono
  • Nested context scopes with withContext() — add per-operation fields without touching the parent scope
  • W3C Trace Context — parses traceparent / tracestate headers inbound, injects them outbound
  • OpenTelemetry — auto-merges active span's traceId and spanId when @opentelemetry/api is present (no-op if not installed)
  • Axios interceptor — propagates trace headers to downstream HTTP calls
  • ESM + CJS dual output, TypeScript-first

Framework Adapters

Express

import { createExpressMiddleware } from 'pino-ctx'

app.use(createExpressMiddleware())

// or with custom context extraction:
app.use(createExpressMiddleware({
  extractContext: (req) => ({
    requestId: req.headers['x-request-id'] ?? req.id,
    userId: req.user?.id,
    tenantId: req.headers['x-tenant-id'],
  })
}))

Fastify

import { pinoCTXPlugin } from 'pino-ctx/fastify'

await app.register(pinoCTXPlugin, {
  extractContext: (request) => ({
    requestId: request.id,
    userId: request.user?.id,
  })
})

Koa

import { createKoaMiddleware } from 'pino-ctx/koa'

app.use(createKoaMiddleware())

Hono

import { createHonoMiddleware } from 'pino-ctx/hono'

app.use(createHonoMiddleware())

Nested Contexts

Scope extra fields to a specific operation without affecting the parent:

import { logger, withContext } from 'pino-ctx'

async function processOrder(orderId: string) {
  await withContext({ orderId }, async () => {
    logger.info('processing order')
    // → {"requestId":"abc","userId":42,"orderId":"ord-99","msg":"processing order"}

    await withContext({ step: 'payment' }, async () => {
      logger.info('charging card')
      // → {"requestId":"abc","userId":42,"orderId":"ord-99","step":"payment","msg":"charging card"}
    })

    logger.info('order done')
    // → {"requestId":"abc","userId":42,"orderId":"ord-99","msg":"order done"}  (step is gone)
  })
}

Works correctly under Promise.all — each branch keeps its own isolated context.


Microservices / Distributed Tracing

Propagate trace context to downstream HTTP calls with the Axios interceptor:

import axios from 'axios'
import { createAxiosContextInterceptor } from 'pino-ctx/axios'
import { getLogContext } from './logger'

const client = axios.create({ baseURL: 'http://orders-service' })
client.interceptors.request.use(createAxiosContextInterceptor(getLogContext))

// Now every outbound request carries:
// traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
// x-request-id: abc-123

OpenTelemetry

Pass includeOpenTelemetryContext: true when setting up your logger and pino-ctx will automatically pull traceId, spanId, and traceFlags from the active OTel span:

const { logger } = createContextLogger({
  includeOpenTelemetryContext: true,
})

// logs automatically include:
// {"traceId":"4bf92f35...","spanId":"00f067aa...","msg":"..."}

Requires @opentelemetry/api to be installed. Falls back silently if it's not.


Performance

pino-ctx uses pino's native hooks.logMethod rather than a Proxy wrapper. Context injection still has measurable overhead, but it stays on pino's supported extension path and avoids wrapping the logger API itself.

plain pino:   lower baseline
pino-ctx:     slightly higher
overhead:     benchmark- and workload-dependent

The included benchmark prints both overall and steady-state overhead. Run it yourself: npm run bench


API Reference

Core

| Export | Description | |--------|-------------| | createContextLogger(options) | Create a logger instance with its own context store | | logger | Default logger (pre-configured instance) | | setContext(ctx) | Merge fields into the current async context | | getContext() | Read the current context | | withContext(ctx, fn) | Run fn in a child context scope | | clearContext() | Reset the current context to {} | | getLogContext() | Read the context as pino will see it (includes OTel if enabled) | | createContextStore() | Create an isolated AsyncLocalStorage store |

Adapters

| Import path | Export | |-------------|--------| | pino-ctx | createExpressMiddleware, pinoCTXMiddleware | | pino-ctx/fastify | pinoCTXPlugin | | pino-ctx/koa | createKoaMiddleware | | pino-ctx/hono | createHonoMiddleware |

Trace / Propagation

| Import path | Export | |-------------|--------| | pino-ctx | createAxiosContextInterceptor | | pino-ctx/propagation | extractTraceContext, injectTraceContextHeaders, parseTraceparentHeader, serializeTraceparentHeader | | pino-ctx/telemetry | getActiveTraceContext, getOpenTelemetryContext |


setContext vs withContext

| | setContext(ctx) | withContext(ctx, fn) | |---|---|---| | Scope | Mutates the current async context | Creates a child scope, isolated to fn | | Use case | Inside middleware where the whole request is already scoped | Per-operation context (a job, a sub-transaction) | | Leaks? | If called outside a scoped context, yes | Never |

Rule of thumb: prefer withContext. Use setContext only inside framework middleware that already owns the request lifetime.


Installation by Use Case

# Core (always required)
npm install pino-ctx pino

# Express
npm install express

# Fastify
npm install fastify

# Koa
npm install koa

# Hono
npm install hono

# Axios propagation
npm install axios

# OpenTelemetry integration
npm install @opentelemetry/api

Requirements

  • Node.js >= 18 (AsyncLocalStorage stable)
  • Pino >= 8

Contributing

npm install
npm run typecheck
npm test
npm run build

See examples/ for runnable Express, Fastify, and microservices demos.


License

MIT