-
-
Notifications
You must be signed in to change notification settings - Fork 494
Description
What is the problem this feature would solve?
I'd like to propose adding first-class TracingChannel support to Elysia, following the pattern established by undici in Node.js core and adopted by framework peers like h3 and srvx, also fastify.
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. It is currently supported across all runtimes (Node.js, Bun, Deno, Cloudflare Workers).
What is the feature you are proposing to solve the problem?
Elysia already has two observability mechanisms:
.trace()API@elysiajs/opentelemetry
I think tracing channels should be its own layer, because .trace has a front facing API, also we want the tracing channels to be working at all times not just when users hit .trace, note that when there are no subscribers the overhead becomes zero.
What I'm proposing is something like:
Request arrives
│
├── TracingChannel `start` fires (request-level, in core request path)
│ → APMs bind async context here
│ → context: method, url, route, headers, params
│
├── .trace() fires per-lifecycle-phase (unchanged, Elysia-specific)
│ → Powers Elysia's built-in observability API
│ → OTel plugin continues to use this for child spans
│
└── TracingChannel `asyncEnd` or `error` fires
→ APMs finalize the request span
→ context enriched: statusCode, responseHeaders
I think we can start with a single elysia:request channel with a lifecycle field — the same pattern h3 uses with its type: "middleware" | "route" field. Each tracePromise call creates its own start/asyncEnd pair, and they nest naturally via async context propagation:
Request arrives
tracePromise({ lifecycle: 'onRequest', type: 'hook', name: 'rateLimiter' })
tracePromise({ lifecycle: 'onParse', type: 'hook', name: 'jsonParser' })
tracePromise({ lifecycle: 'onBeforeHandle', type: 'hook', name: 'authCheck' })
tracePromise({ lifecycle: 'handle', type: 'handler', name: 'getUser' })
tracePromise({ lifecycle: 'onAfterHandle', type: 'hook', name: 'addCors' })
tracePromise({ lifecycle: 'onAfterResponse', type: 'hook', name: 'logger' })
Whether or not @elysia/opentelemetry decides to use this over .trace is entirely optional.
Overhead and Backward Compatibility
Zero-cost when no subscribers are registered, hasSubscribers is checked before wrapping any hook or handler with tracePromise. We can also silently skip on runtimes where TracingChannel is unavailable.
Usually we do this in Node.js, but we can see if it works with Bun as well or not.
let requestChannel;
try {
const dc = ('getBuiltinModule' in process)
? process.getBuiltinModule('node:diagnostics_channel')
: require('node:diagnostics_channel');
requestChannel = dc.tracingChannel('elysia:request');
} catch {
// TracingChannel not available — no-op
}I'm happy to spec this more in a PR and see where it goes from there.
Example
Users and APM alike can subscribe to the tracing channels and do whatever they need to do, this is just a rough example:
const dc = require('node:diagnostics_channel');
dc.tracingChannel('elysia:request').subscribe({
start(ctx) {
const tracer = trace.getTracer('elysia');
const spanName = ctx.type === 'handler'
? `${ctx.event.request.method} ${ctx.event.route}`
: `${ctx.lifecycle}: ${ctx.name}`;
ctx.span = tracer.startSpan(spanName, {
kind: ctx.type === 'handler' ? SpanKind.SERVER : SpanKind.INTERNAL,
attributes: {
'http.request.method': ctx.event.request.method,
'http.route': ctx.event.route,
'elysia.lifecycle': ctx.lifecycle,
},
});
// TracingChannel automatically propagates this span as the active context —
// lifecycle phases nest naturally via async context, no manual .wrap() needed
},
asyncEnd(ctx) {
if (ctx.type === 'handler') {
ctx.span?.setAttribute('http.response.status_code', ctx.event.set.status);
}
ctx.span?.end();
},
error(ctx) {
ctx.span?.setStatus({ code: SpanStatusCode.ERROR, message: ctx.error?.message });
ctx.span?.recordException(ctx.error);
},
});