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:
- 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.
- 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.
I'd like to propose adding first-class
TracingChannelsupport toknex, following the pattern established byundiciin 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, andquery-errorevents viaEventEmitter. However, APM instrumentation libraries cannot use these events for span-based tracing today. Instead, they resort to monkey-patchingClient.prototype.queryBuilder,Client.prototype.schemaBuilder,Client.prototype.raw, andRunner.prototype.queryvia IITM/RITM. Here's why:AsyncLocalStoragecontext. This is explicitly identified as a limitation in knex#6270.queryandquery-response/query-errorare separate EventEmitter events. To build a span, APM tools must correlate them via__knexUidand 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:
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.knexis firstrequire()'d /import'd.TracingChannelsolves 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
TracingChannelAPI, which providesstart,end,asyncStart,asyncEnd, anderrorsub-channels automatically.We will certainly need a
knex:querychannel, along with aknex:transactionat 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):
Prior Art
This approach follows the same pattern already adopted or in progress by other major libraries:
undici(Node.js core) — shipsTracingChannelsupport since Node 20.12:undici:requestnode-redis— redis/node-redis#3195 (node-redis:command,node-redis:connect)ioredis— redis/ioredis#2089 (ioredis:command,ioredis:connect)pg/pg-pool— brianc/node-postgres#3624 (pg:query,pg:connection,pg:pool:connect)mysql2— sidorares/node-mysql2#4178 (mysql2:query,mysql2:execute,mysql2:connect,mysql2:pool:connect)I'm happy to spec this out in a PR and we can take it from there.