Skip to content

TracingChannel Proposal for observability #6394

@logaretm

Description

@logaretm

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

This is similar to #4651 but advocates using tracing channels as a higher level API instead.

Motivation

Knex already emits query, query-response, and query-error events via EventEmitter. However, APM instrumentation libraries cannot use these events for span-based tracing today. Instead, they resort to monkey-patching Client.prototype.queryBuilder, Client.prototype.schemaBuilder, Client.prototype.raw, and Runner.prototype.query 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. This is explicitly identified as a limitation in knex#6270.
  2. Split lifecycle. query and query-response/query-error are separate EventEmitter events. To build a span, APM tools must correlate them via __knexUid and manage span storage manually. TracingChannel provides a unified lifecycle for the operation, so correlation is built-in.

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 knex 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.

Proposed API

I took a look at #6270 and while it works for users, it doesn't work for APMs for the outlined reasons above. So instead, I suggest we create the following tracing channels:

All channels use the Node.js TracingChannel API, which provides start, end, asyncStart, asyncEnd, and error sub-channels automatically.

We will certainly need a knex:query channel, along with a knex:transaction at the very least, it is possible to have more to cover pool events. I will know more once everything's into a PR.

This is what it would look like for consumers (users and APMs alike):

const dc = require('node:diagnostics_channel');

// Subscribe to query execution
dc.tracingChannel('knex:query').subscribe({
  start(ctx) {
    ctx.span = tracer.startSpan(`${ctx.method} ${ctx.table}`, {
      attributes: {
        'db.system': 'postgresql',  // or mysql, sqlite, etc.
        'db.operation.name': ctx.method,
        'db.sql.table': ctx.table,
        'db.query.text': ctx.query,
        'db.namespace': ctx.database,
        'server.address': ctx.serverAddress,
        'server.port': ctx.serverPort,
      },
    });
  },
  asyncEnd(ctx) {
    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 major libraries:


I'm happy to spec this out in a PR and we can take it from there.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions