Skip to content

Support Tracing Channels #1727

@logaretm

Description

@logaretm

I'd like to propose adding first-class TracingChannel support to tedious, following the pattern established by undici in Node.js core.

The tedious driver already provides operational visibility through a rich EventEmitter API. However, APM instrumentation libraries (OpenTelemetry, Datadog, Sentry) cannot use these events for span-based tracing today. Instead, they resort to monkey-patching Connection.prototype.execSql, Connection.prototype.callProcedure, and other methods via IITM/RITM. Here's why:

  1. No async context propagation. EventEmitter listeners run in the emitter's execution context, not the caller's AsyncLocalStorage context. APM tools need to create child spans of the caller's span, which requires capturing the async context at the call site. EventEmitter listeners lose this context.

  2. Split lifecycle. The Request callback and its done/doneProc/error events are separate. To build a span, APM tools must wrap the original callback and attach additional event listeners to count statements and procedures. TracingChannel provides a unified lifecycle for the operation, so correlation is built-in.

  3. Multiple interception points. OTel currently patches 6 separate methods on Connection.prototype, Each patch must independently handle callback wrapping, context propagation, and error handling. Native TracingChannel support consolidates this into structured event emission at the source.

Beyond these APM-specific gaps, the current monkey-patching approach has broader ecosystem concerns:

  • Runtime lock-in: RITM and IITM rely on Node.js-specific module loader internals (Module._resolveFilename, module.register()). They don't work on Bun or Deno, which implement the Node.js API surface but not the module loader internals.
  • ESM fragility: IITM is built on Node.js's module customization hooks, which are still evolving and have been a persistent source of breakage in the OTEL JS ecosystem.
  • Initialization ordering: Both require instrumentation to be set up before tedious is first require()'d / import'd.
  • Bundling: Users must ensure instrumented modules are externalized, which is increasingly difficult as frameworks bundle server-side code into single executables or deployment files.

TracingChannel solves all of these. It provides structured lifecycle events (start, end, asyncStart, asyncEnd, error) with built-in async context propagation, zero-cost when no subscribers are attached, and a standardized subscription model that requires no monkey-patching.

I'm happy to spec this out in a PR and we can discuss it there if you feel like it's a worthwhile addition to tedious.

Prior Art

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions