-
-
Notifications
You must be signed in to change notification settings - Fork 4k
TracingChannel Proposal for observability #16105
Description
Prerequisites
- I have written a descriptive issue title
- I have searched existing issues to ensure the feature has not already been requested
🚀 Feature Proposal
I'd like to propose adding first-class TracingChannel support to mongoose, following the pattern established by undici in Node.js core.
Motivation
Mongoose already has a powerful pre/post middleware (hook) system that covers document, query, aggregate, and model operations.
However, APM instrumentation libraries cannot use these hooks for span-based tracing today. Instead, they resort to monkey-patching Query.prototype.exec, Aggregate.prototype.exec, Model.prototype.save, and 12+ other methods via IITM/RITM. Here's why:
-
No async context propagation. Mongoose hooks run in the middleware chain's execution context, not the caller's
AsyncLocalStoragecontext. This is a documented pain point: #10020 reported post-hooks not working withAsyncLocalStorage, and #10478 identified the root cause as an underlying library issue where async context is lost across the hook chain. While these issues have been resolved, they illustrate that async context propagation through the middleware system has been a persistent challenge, and it's exactly the problemTracingChannelwas designed to solve at the platform level. -
Per-schema registration. Hooks must be registered on each schema individually. APM tools need global instrumentation that covers all models without requiring user configuration. There's no single attachment point for cross-cutting tracing concerns.
-
No lifecycle correlation. Pre and post hooks are separate middleware functions. To build a span, APM tools would need to correlate the pre-hook (span start) with the post-hook (span end) and manage span storage manually.
TracingChannelprovides 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
mongooseis firstrequire()'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.
Example
I propose implementing the following tracing channels:
| TracingChannel | Tracks | Context fields |
|---|---|---|
mongoose:query |
All Query.exec() operations — find, update, delete, count, distinct, etc. |
operation, collection, query, fields, options, database, serverAddress, serverPort |
mongoose:aggregate |
Aggregate.exec() pipeline execution |
pipeline, collection, options, database, serverAddress, serverPort |
mongoose:save |
Model.prototype.save() — document insert or update |
operation, collection, database, serverAddress, serverPort |
mongoose:model |
Model-level statics — insertMany, bulkWrite |
operation, collection, database, serverAddress, serverPort |
The usage will look something like this:
const dc = require('node:diagnostics_channel');
// Subscribe to query operations, covers ALL find/update/delete/count variants
dc.tracingChannel('mongoose:query').subscribe({
start(ctx) {
// TracingChannel automatically propagates this span as the active context
// through the entire async operation, no manual context.with() needed
ctx.span = tracer.startSpan(`${ctx.operation} ${ctx.collection}`, {
attributes: {
'db.system': 'mongodb',
'db.operation.name': ctx.operation,
'db.collection.name': ctx.collection,
'db.query.text': sanitize(ctx.query),
'db.namespace': ctx.database,
'server.address': ctx.serverAddress,
'server.port': ctx.serverPort,
},
});
},
asyncEnd(ctx) {
// asyncEnd fires after the promise resolves — this is where the span ends
ctx.span?.end();
},
error(ctx) {
ctx.span?.setStatus({ code: SpanStatusCode.ERROR, message: ctx.error?.message });
ctx.span?.recordException(ctx.error);
},
});The context payload can match what OTEL currently has.
I'm happy to help spec this out in a PR and see what you folks think.
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-redisredis/node-redis#3195 (node-redis:command,node-redis:connect)ioredisredis/ioredis#2089 (ioredis:command,ioredis:connect)pg/pg-poolbrianc/node-postgres#3624 (pg:query,pg:connection,pg:pool:connect)mysql2sidorares/node-mysql2#4178 (mysql2:query,mysql2:execute,mysql2:connect,mysql2:pool:connect)
Full Disclosure, I'm driving the adoption process in some of these projects