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

@marceloraineri/async-context

v1.1.1

Published

> Async context propagation and structured logging for Node.js, with first-class framework integrations.

Readme

AsyncContext

Async context propagation and structured logging for Node.js, with first-class framework integrations.

AsyncContext helps you carry contextual data across asynchronous boundaries without passing parameters through every function. It provides a Context wrapper around AsyncLocalStorage, ready-made middleware for popular frameworks, and a structured logger that automatically includes the active context in every log entry.

Highlights

  • Consistent per-request context without parameter threading.
  • Simple, direct API for reading and writing context anywhere in the async flow.
  • Observability-ready with correlation IDs, tenant/user data, and tracing metadata.
  • Full-featured logger with levels, redaction, sampling, timers, and transports.
  • DX-friendly configuration via presets and environment variables.
  • Zero runtime dependencies and performance-focused design.

Installation

npm i @marceloraineri/async-context

Quick start

import crypto from "node:crypto";
import { Context } from "@marceloraineri/async-context";

await Context.run({ requestId: crypto.randomUUID() }, async () => {
  Context.addValue("user", { id: 42, name: "Ada" });
  Context.addOptions({ feature: "beta", retry: 2 });

  await Promise.resolve();

  const store = Context.getStore();
  console.log(store?.requestId); // 184fa9a3-f967-4a98-9d8f-57152e7cbe64
  console.log(store?.user); // { id: 42, name: "Ada" }
  console.log(store?.options); // { feature: "beta", retry: 2 }
});

Structured logging

The logger automatically merges the active async context and supports redaction, sampling, timers, and JSON or pretty output.

import crypto from "node:crypto";
import { Context, createLogger } from "@marceloraineri/async-context";

const logger = createLogger({
  name: "api",
  level: "info",
  contextKey: "ctx",
  redactKeys: ["ctx.token", "data.password"],
});

await Context.run({ requestId: crypto.randomUUID(), token: "secret" }, async () => {
  logger.info("request started", { route: "/ping" });
});

By default, common sensitive keys (for example password, token, authorization) are automatically redacted. You can disable this with redactDefaults: false or add extra key names with redactFieldNames.

Timers and child loggers

import { createLogger } from "@marceloraineri/async-context";

const logger = createLogger({ name: "jobs", level: "debug" });
const jobLogger = logger.child({ job: "import-users" });

const end = jobLogger.startTimer("debug");
await Promise.resolve();
end("job completed");

const noisyLogger = logger.child({ job: "debug-import" }, { level: "trace" });
noisyLogger.trace("verbose logging enabled");

JSON output or custom transports

import { createConsoleTransport, createLogger } from "@marceloraineri/async-context";

const logger = createLogger({
  level: "info",
  transports: [createConsoleTransport({ format: "json" })],
});

logger.info("structured log", { feature: "json" });

OpenAI wrapper

Capture OpenAI call metadata inside the current async context.

import OpenAI from "openai";
import { Context, withOpenAIContext } from "@marceloraineri/async-context";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

await Context.run({ requestId: "req_123" }, async () => {
  const response = await withOpenAIContext(
    "responses.create",
    { model: "gpt-4o", input: "Hello!" },
    (req) => openai.responses.create(req),
    { includeRequest: true }
  );

  console.log(response.id);
  console.log(Context.getValue("openai"));
});

By default, the wrapper appends summaries to the openai context key, and only includes safe request fields unless you explicitly allow more keys.

OpenTelemetry (optional)

Create spans, propagate trace context from headers, and sync baggage with AsyncContext when @opentelemetry/api is available.

import * as otel from "@opentelemetry/api";
import {
  Context,
  createAsyncContextExpressOpenTelemetryMiddleware,
  setOpenTelemetryBaggageFromContext,
  withOpenTelemetrySpan,
} from "@marceloraineri/async-context";

app.use(
  createAsyncContextExpressOpenTelemetryMiddleware({
    otel: {
      api: otel,
      tracerName: "api",
      contextAttributeKeys: ["requestId", "tenantId"],
    },
  })
);

await Context.run({ requestId: "req_1", tenantId: "t_123" }, async () => {
  await withOpenTelemetrySpan(
    "db.query",
    async () => Promise.resolve(),
    { api: otel, attributes: { db: "users" } }
  );

  setOpenTelemetryBaggageFromContext({
    api: otel,
    contextKeys: ["requestId", "tenantId"],
    baggagePrefix: "ctx.",
  });
});

Performance timing

Measure sync or async work and store timing data in the active context.

import { Context } from "@marceloraineri/async-context";

await Context.run({}, async () => {
  await Context.measure("db.query", async () => {
    await Promise.resolve();
  }, { data: { table: "users" } });

  console.log(Context.getValue("perf"));
});

Use key or mode to control where entries are stored.

Context.measure("cache.lookup", () => "hit", { key: "performance", mode: "overwrite" });

DX and configuration

Use presets or environment variables to configure logging without code changes.

import { createLoggerFromEnv, loggerPreset } from "@marceloraineri/async-context";

const logger = createLoggerFromEnv({
  name: "api",
  defaults: loggerPreset("production"),
});

Environment variables:

| Variable | Description | Example | | --- | --- | --- | | LOG_PRESET | development, production, or test preset | production | | LOG_LEVEL | Minimum log level | info | | LOG_FORMAT | json or pretty | json | | LOG_COLORS | Enable ANSI colors | true | | LOG_CONTEXT | Attach async context | true | | LOG_CONTEXT_KEY | Key name for context | ctx | | LOG_CONTEXT_KEYS | Comma-separated allowlist | requestId,tenantId | | LOG_REDACT_KEYS | Comma-separated redaction paths | ctx.token,data.password | | LOG_REDACT_DEFAULTS | Enable default sensitive-field redaction | true | | LOG_REDACT_FIELDS | Extra sensitive field names (comma-separated) | accessToken,creditCard | | LOG_REDACT_PLACEHOLDER | Mask value placeholder | [REDACTED] | | LOG_SAMPLE_RATE | 0..1 sampling | 0.25 | | LOG_INCLUDE_PID | Include process id | true | | LOG_INCLUDE_HOSTNAME | Include hostname | false | | LOG_TIMESTAMP | Include timestamp | true | | LOG_NAME | Logger name | api |

Framework integrations

Express

AsyncContextExpresssMiddleware (with three "s") and AsyncContextExpressMiddleware (alias) create a new context per request and seed it with a unique instance_id.

import express from "express";
import { AsyncContextExpressMiddleware, Context } from "@marceloraineri/async-context";

const app = express();
app.use(AsyncContextExpressMiddleware);

app.get("/ping", (_req, res) => {
  const store = Context.getStore();
  res.json({ instanceId: store?.instance_id ?? null });
});

app.listen(3000, () => console.log("API listening on :3000"));

If you need a custom request id or seed data, use createAsyncContextExpressMiddleware.

import express from "express";
import { createAsyncContextExpressMiddleware, Context } from "@marceloraineri/async-context";

const app = express();

app.use(
  createAsyncContextExpressMiddleware({
    idKey: "request_id",
    seed: (req) => ({ method: req.method, path: req.url }),
  })
);

app.get("/ping", (_req, res) => {
  const store = Context.getStore();
  res.json({ requestId: store?.request_id ?? null });
});

Fastify

Use the onRequest hook or the convenience registration helper.

import fastify from "fastify";
import { createAsyncContextFastifyHook, Context } from "@marceloraineri/async-context";

const app = fastify();
app.addHook("onRequest", createAsyncContextFastifyHook());

app.get("/ping", async () => {
  const store = Context.getStore();
  return { instanceId: store?.instance_id ?? null };
});

Koa

import Koa from "koa";
import { createAsyncContextKoaMiddleware, Context } from "@marceloraineri/async-context";

const app = new Koa();
app.use(createAsyncContextKoaMiddleware());

app.use(async (ctx) => {
  const store = Context.getStore();
  ctx.body = { instanceId: store?.instance_id ?? null };
});

Next.js API routes

import type { NextApiRequest, NextApiResponse } from "next";
import { createAsyncContextNextHandler, Context } from "@marceloraineri/async-context";

export default createAsyncContextNextHandler(
  async (_req: NextApiRequest, res: NextApiResponse) => {
    const store = Context.getStore();
    res.status(200).json({ instanceId: store?.instance_id ?? null });
  }
);

NestJS

AsyncContextNestMiddleware reuses the Express integration to enable async context in Nest (Express adapter).

import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { AsyncContextNestMiddleware } from "@marceloraineri/async-context";

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(AsyncContextNestMiddleware).forRoutes("*");
  }
}

AdonisJS

AsyncContextAdonisMiddleware plugs into the AdonisJS pipeline and creates one context per request.

// app/Http/Middleware/AsyncContext.ts
import { AsyncContextAdonisMiddleware } from "@marceloraineri/async-context";

export default AsyncContextAdonisMiddleware;

Sentry (optional)

AsyncContext can enrich Sentry events with the active store. If @sentry/node is not installed, the helpers safely no-op. Sensitive fields are redacted by default. Disable with redactDefaults: false or add redactFieldNames.

import express from "express";
import {
  AsyncContextExpressMiddleware,
  initSentryWithAsyncContext,
  sentryAsyncContextExpressMiddleware,
  sentryErrorHandler,
} from "@marceloraineri/async-context";

initSentryWithAsyncContext({
  sentryInit: {
    dsn: process.env.SENTRY_DSN,
    tracesSampleRate: 1.0,
  },
  redactKeys: ["async_context.token"],
});

const app = express();
app.use(AsyncContextExpressMiddleware);
app.use(sentryAsyncContextExpressMiddleware());

app.get("/ping", (_req, res) => res.json({ ok: true }));

app.use(sentryErrorHandler());

API overview

  • Context.run(store, callback) and Context.run(callback)
  • Context.getStore() and Context.requireStore()
  • Context.getValue(key) and Context.requireValue(key)
  • Context.addValue(key, value) and Context.addObjectValue(values)
  • Context.runWith(values, callback)
  • Context.snapshot() and Context.reset()
  • createLogger(options) and new Logger(options)
  • Logger.child(bindings, options?), Logger.withBindings(bindings, callback, options?), and Logger.startTimer(level?)
  • createConsoleTransport(options)
  • createLoggerFromEnv(options) and loggerPreset(preset)
  • parseBooleanEnv(value), parseNumberEnv(value), parseCsvEnv(value), parseLogLevelEnv(value), parseLogFormatEnv(value), parseLoggerPresetEnv(value)
  • createAsyncContextExpressMiddleware(options)
  • createAsyncContextFastifyHook(options) and registerAsyncContextFastify(app, options)
  • createAsyncContextKoaMiddleware(options)
  • createAsyncContextNextHandler(handler, options)
  • withOpenTelemetrySpan(name, callback, options) and recordOpenTelemetrySpan(summary, options)
  • createAsyncContextExpressOpenTelemetryMiddleware(options)
  • createAsyncContextFastifyOpenTelemetryHook(options)
  • createAsyncContextKoaOpenTelemetryMiddleware(options)
  • createAsyncContextNextOpenTelemetryHandler(handler, options)
  • setOpenTelemetryBaggageFromContext(options) and mergeContextFromOpenTelemetryBaggage(options)
  • extractOpenTelemetryContextFromHeaders(headers, options) and injectOpenTelemetryContextToHeaders(headers, options)
  • initSentryWithAsyncContext(options) and captureExceptionWithContext(error)

Best practices

  • Avoid replacing the entire store object; prefer addValue and addObjectValue.
  • Long-lived contexts can retain memory; always complete the flow (next() in middleware).
  • AsyncLocalStorage is per-process, so each worker maintains its own context.

Contributing

Issues and pull requests are welcome. Include repro steps, tests, or concrete usage scenarios when possible.