Prerequisites
🚀 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 AsyncLocalStorage context. This is a documented pain point: #10020 reported post-hooks not working with AsyncLocalStorage, 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 problem TracingChannel was 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. 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
mongoose 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.
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:
Full Disclosure, I'm driving the adoption process in some of these projects
Prerequisites
🚀 Feature Proposal
I'd like to propose adding first-class
TracingChannelsupport tomongoose, following the pattern established byundiciin 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:
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.mongooseis 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.Example
I propose implementing the following tracing channels:
mongoose:queryQuery.exec()operations — find, update, delete, count, distinct, etc.operation,collection,query,fields,options,database,serverAddress,serverPortmongoose:aggregateAggregate.exec()pipeline executionpipeline,collection,options,database,serverAddress,serverPortmongoose:saveModel.prototype.save()— document insert or updateoperation,collection,database,serverAddress,serverPortmongoose:modelinsertMany,bulkWriteoperation,collection,database,serverAddress,serverPortThe usage will look something like this:
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