Skip to content

TracingChannel Proposal for observability #16105

@logaretm

Description

@logaretm

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:

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

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementThis issue is a user-facing general improvement that doesn't fix a bug or add a new featurenew featureThis change adds new functionality, like a new method or class

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions