-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Observability through TracingChannels API #4842
Description
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:
hono/timing: a middleware that addsServer-Timingheaders withsetMetric,startTime,endTime. Useful for browser DevTools but not for APM/distributed tracing.@hono/otel: - the official OTel middleware inhonojs/middleware. It uses@opentelemetry/apidirectly (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:
- It mirrors h3's and Elysia's pattern. h3 uses one
h3.requestchannel withtype: "middleware" | "route". Elysia uses oneelysia:requestchannel withlifecycleandtypefields. Consistency across frameworks means APM tools can share subscriber logic. - APMs subscribe once. A single subscription captures the full request middleware tree. The nesting comes from async context propagation, not from multiple channel subscriptions.
- 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): shipsTracingChannelsupport since Node 20.12 (undici:request)fastify: shipsTracingChannelsupport natively (tracing:fastify.request.handler)h3: h3js/h3#1251 (h3.request, traces middleware and route handlers withtypefield)mysql2: sidorares/node-mysql2#4178 (mysql2:query,mysql2:execute,mysql2:connect,mysql2:pool:connect) ✅ mergednode-redis: redis/node-redis#3195 (node-redis:command,node-redis:connect), approved, pending mergeioredis: 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.