Skip to content

Observability through TracingChannels API #4842

@logaretm

Description

@logaretm

What is the feature you are proposing?

I'd like to propose adding first-class TracingChannel support to Hono, following the pattern established by undici in Node.js core and adopted by framework peers like h3, fastify, and srvx.

TracingChannel is a higher-level API built on top of diagnostics_channel, specifically designed for tracing async operations. It provides structured lifecycle channels (start, end, error, asyncStart, asyncEnd) and handles async context propagation correctly, this is the missing piece that makes monkey-patching approaches fragile in real-world async applications.

We (at Sentry) are currently working on an SDK for hono, and right. now we have to monkey patch 9 methods to ensure all paths are covered. On certain runtimes this gets tricky since those patches need to be re-applied each time a request comes in.

The Sentry case is particularly instructive: a single APM vendor had to build two completely separate instrumentation approaches for the same framework. One for Node.js (IITM-based) and one for Cloudflare Workers (Proxy + middleware-based) and explicitly filters out the Node integration when running on Workers to avoid double-instrumentation. This is the kind of complexity that native TracingChannel support eliminates entirely.

With TracingChannel, a single diagnostics_channel subscription works identically across Node.js, Cloudflare Workers, and any other runtime that supports the API. No IITM, no Proxy hacks, no per-runtime code paths, no user code changes.

If Hono emits structured events through TracingChannel, instrumentation libraries become subscribers, not patches. Each tool listens independently with no ordering concerns, no clobbering, and no internal API dependency.

Where TracingChannel Fits: Relationship to @hono/otel and Built-in Middleware

Hono has two existing observability mechanisms:

  1. hono/timing: a middleware that adds Server-Timing headers with setMetric, startTime, endTime. Useful for browser DevTools but not for APM/distributed tracing.
  2. @hono/otel: - the official OTel middleware in honojs/middleware. It uses @opentelemetry/api directly (tracer.startActiveSpan()) to create a root server span and record request metrics. It is a standard Hono middleware, not a framework-level instrumentation.

Neither mechanism provides what TracingChannel adds:

Concern hono/timing @hono/otel TracingChannel (proposed)
Activation User must add middleware User must add middleware Automatic; subscribers attach externally
Scope Timing metrics only Root request span + metrics Per-middleware and per-handler spans with full async lifecycle
Async context N/A Manual context.with() in middleware Built-in. TracingChannel propagates AsyncLocalStorage context through the middleware cascade
Multi-vendor N/A OTel-only Any diagnostics_channel subscriber (OTel, Datadog, Sentry, custom)
Middleware visibility Only what user explicitly measures Single root span, no per-middleware breakdown Each middleware and handler traced individually

TracingChannel complements both. hono/timing continues to serve its purpose for HTTP header-based timing, and @hono/otel can be simplified to a thin TracingChannel subscriber instead of managing its own span lifecycle.

Proposed Tracing Channel

TracingChannel Tracks Context fields
hono:request Individual middleware and route handler executions within the request type, name, ctx

A single channel with a type field (rather than separate channels per middleware) because:

  1. It mirrors h3's and Elysia's pattern. h3 uses one h3.request channel with type: "middleware" | "route". Elysia uses one elysia:request channel with lifecycle and type fields. Consistency across frameworks means APM tools can share subscriber logic.
  2. APMs subscribe once. A single subscription captures the full request middleware tree. The nesting comes from async context propagation, not from multiple channel subscriptions.
  3. Hono's middleware cascade is a continuum. Unlike database operations (query vs connect) which are semantically distinct, middleware functions are stages of the same request. One channel reflects this.

The ctx object can contain useful information about the request, like the request object itself, matched route, etc... But we can spec this out in a PR.

Multi-Runtime Considerations

Hono is runtime-agnostic, and diagnostics_channel is available across Node.js, Cloudflare Workers, and is expanding to other runtimes. The standard compatibility pattern handles any runtime gracefully:

let requestChannel;
try {
  const dc = ('getBuiltinModule' in process)
    ? process.getBuiltinModule('node:diagnostics_channel')
    : require('node:diagnostics_channel');
  requestChannel = dc.tracingChannel('hono:request');
} catch {
  // diagnostics_channel not available on this runtime, no-op
}

Usage Example

import dc from 'node:diagnostics_channel';

dc.tracingChannel('hono:request').subscribe({
  start(ctx) {
    const spanName = ctx.type === 'handler'
      ? `${ctx.c.req.method} ${ctx.c.req.routePath}`
      : `middleware - ${ctx.name}`;

    ctx.span = tracer.startSpan(spanName, {
      kind: ctx.type === 'handler' ? SpanKind.SERVER : SpanKind.INTERNAL,
      attributes: {
        'http.request.method': ctx.c.req.method,
        'http.route': ctx.c.req.routePath,
        'hono.type': ctx.type,
        'hono.name': ctx.name,
      },
    });
    // TracingChannel automatically propagates this span as the active context.
    // Middleware cascade nests naturally via async context, no manual wrapping needed.
  },
  asyncEnd(ctx) {
    if (ctx.type === 'handler') {
      ctx.span?.setAttribute('http.response.status_code', ctx.c.res.status);
    }
    ctx.span?.end();
  },
  error(ctx) {
    ctx.span?.setStatus({ code: SpanStatusCode.ERROR, message: ctx.error?.message });
    ctx.span?.recordException(ctx.error);
  },
});

Prior Art

This approach follows the same pattern already adopted or in progress by other libraries:

Frameworks:

  • undici (Node.js core): ships TracingChannel support since Node 20.12 (undici:request)
  • fastify: ships TracingChannel support natively (tracing:fastify.request.handler)
  • h3: h3js/h3#1251 (h3.request, traces middleware and route handlers with type field)
  • mysql2: sidorares/node-mysql2#4178 (mysql2:query, mysql2:execute, mysql2:connect, mysql2:pool:connect) ✅ merged
  • node-redis: redis/node-redis#3195 (node-redis:command, node-redis:connect), approved, pending merge
  • ioredis: redis/ioredis#2089 (ioredis:command, ioredis:connect)
  • pg / pg-pool: brianc/node-postgres#3624 (pg:query, pg:connection, pg:pool:connect)

I will be happy to help spec this out in a PR! I worked on similar implementations for Nitro, Elysia, and several database libraries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions