Skip to content

feat: add TracingChannel support for native APM instrumentation - #4178

Merged
wellwelwel merged 10 commits into
sidorares:masterfrom
logaretm:feat/tracing-channel-support
Mar 14, 2026
Merged

wellwelwel merged 10 commits into
sidorares:masterfrom
logaretm:feat/tracing-channel-support

Conversation

@logaretm

@logaretm logaretm commented Mar 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add first-class TracingChannel (node:diagnostics_channel) support so APM tools (OTEL, Datadog, Sentry) can instrument mysql2 without monkey-patching via IITM/RITM
  • Four tracing channels: mysql2:query, mysql2:execute, mysql2:connect, mysql2:pool:connect
  • Context fields map to OTEL semantic conventions (db.query.text, db.namespace, server.address, server.port)
  • Zero-cost when no subscribers — hasSubscribers checked before any context allocation
  • Graceful degradation on Node versions without TracingChannel (e.g. Node 18)

Motivation

Current APM instrumentations use IITM (import-in-the-middle) for ESM and RITM (require-in-the-middle) for CJS to monkey-patch Connection.prototype.query and Connection.prototype.execute. This has fragility concerns:

  • Runtime lock-in: RITM/IITM rely on Node.js-specific module loader internals — they don't work on Bun or Deno
  • ESM fragility: IITM is built on Node.js's evolving module customization hooks, a persistent source of breakage in the OTEL JS ecosystem
  • Initialization ordering: instrumentation must be set up before mysql2 is first imported — get the order wrong and instrumentation silently does nothing
  • Bundling: users must externalize instrumented modules, increasingly difficult with server-side bundling

With native TracingChannel support, instrumentation libraries become subscribers, not patches. Each tool listens independently with no ordering concerns, no clobbering, and no internal API dependency.

Channels

TracingChannel Tracks Context fields
mysql2:query connection.query() execution query, values, database, serverAddress, serverPort
mysql2:execute connection.execute() prepared statements query, values, database, serverAddress, serverPort
mysql2:connect Client connection lifecycle database, serverAddress, serverPort, user
mysql2:pool:connect Pool connection acquisition database, serverAddress, serverPort

Implementation

  • New lib/tracing.js + lib/tracing.d.ts — channel setup, context types, tracePromise wrappers
  • lib/base/connection.js — instrumented query(), execute(), and constructor (connect)
  • lib/base/pool.js — instrumented getConnection()
  • typings/mysql/index.d.ts — re-exports context types for APM consumers
  • test/integration/tracing-channel.test.mts — integration tests for all 4 channels

Uses tracePromise with promise wrapping (same pattern as node-redis, ioredis, pg implementations). Promise wrapper in lib/promise/ delegates to the base layer, so promise API gets tracing for free.

Prior art

Follows the same pattern adopted by:

closes #4174

Add first-class `TracingChannel` (`node:diagnostics_channel`) support so
APM tools (OTEL, Datadog, Sentry) can instrument mysql2 without
monkey-patching via IITM/RITM.

Four tracing channels are implemented:
- `mysql2:query` — traces `connection.query()` (callback and event-emitter modes)
- `mysql2:execute` — traces `connection.execute()` prepared statements
- `mysql2:connect` — traces initial connection handshake
- `mysql2:pool:connect` — traces pool connection acquisition

Context fields map to OTEL semantic conventions (`db.query.text`,
`db.namespace`, `server.address`, `server.port`). Zero-cost when no
subscribers are registered. Graceful degradation on Node versions
without `TracingChannel` (e.g. Node 18).

Follows the pattern established by undici (Node core), node-redis,
ioredis, and pg.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 13, 2026 21:22

This comment was marked as spam.

@wellwelwel

wellwelwel commented Mar 13, 2026 •

Copy link
Copy Markdown
Collaborator

Sorry about the Copilot spam.

I see you're using Claude, my suggestion would be you to report the errors to it, so it can investigate what caused the current code to break before continuing 🙋🏻‍♂️

For example: https://github.com/sidorares/node-mysql2/actions/runs/23071036612/job/67024378058?pr=4178

@wellwelwel

Copy link
Copy Markdown
Collaborator

@logaretm, do you think the Pool Cluster should have its own channel, or could that conflict with the Pool’s existing channel?

- Short-circuit query()/execute() to original code path when no
  tracing subscribers exist — zero behavioral change for non-instrumented
  users
- Use origCb.call(cmdQuery, ...) to preserve the command instance as
  `this` in callbacks, matching mysql2's existing contract
- Clean up unused connect/error listener in handshake tracing to
  prevent listener leaks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@logaretm

Copy link
Copy Markdown
Contributor Author

Yea, no worries i directed it to the failures.

For the pool cluster, the individual pool connections already get traced via mysql2:pool:connect and mysql2:connect channels, to visulize what that would look like in an APM dashboard or from a user's presective:

  With pool cluster channel:
  mysql2:pool-cluster:connect  [0.2ms]  pattern=SLAVE*, selected=SLAVE2
    └── mysql2:pool:connect    [5ms]    server=slave2-host:3306
        └── mysql2:query       [12ms]   SELECT * FROM users

  Without it (what we already have):
  mysql2:pool:connect          [5ms]    server=slave2-host:3306
    └── mysql2:query           [12ms]   SELECT * FROM users

I don't have a strong opinion on this, OTEL didn't instrument clusters but usually the more channels the merrier as if the APM doesn't subscribe to it then no overhead is introduced. What do you think?

@wellwelwel

wellwelwel commented Mar 13, 2026 •

Copy link
Copy Markdown
Collaborator

I don't have a strong opinion on this, OTEL didn't instrument clusters but usually the more channels the merrier as if the APM doesn't subscribe to it then no overhead is introduced. What do you think?

I think the current channels are already good. Thanks for the example.

@codecov

codecov Bot commented Mar 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.16129% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (cb2df63) to head (9987d30).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
lib/base/connection.js 80.09% 40 Missing ⚠️
lib/tracing.js 92.59% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4178      +/-   ##
==========================================
- Coverage   90.44%   90.32%   -0.13%     
==========================================
  Files          86       87       +1     
  Lines       13989    14280     +291     
  Branches     1733     1788      +55     
==========================================
+ Hits        12653    12898     +245     
- Misses       1336     1382      +46     
Flag Coverage Δ
compression-0 89.57% <85.16%> (-0.12%) ⬇️
compression-1 90.30% <85.16%> (-0.13%) ⬇️
static-parser-0 88.01% <85.16%> (-0.08%) ⬇️
static-parser-1 88.73% <85.16%> (-0.10%) ⬇️
tls-0 89.75% <85.16%> (-0.12%) ⬇️
tls-1 90.10% <85.16%> (-0.13%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ilable

poku's describe.skip still executes test bodies, causing failures
on Node 18. Use process.exit(0) instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread test/integration/tracing-channel.test.mts
Comment thread lib/tracing.js
…testability

- Replace process.exit(0) with poku's skip() for better test reporting
- Export dc and hasTracingChannel from tracing.js for testing
- Add unit test for tracing module (dc availability, channel setup, getServerContext)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@logaretm
logaretm force-pushed the feat/tracing-channel-support branch from d294092 to cd7d6f3 Compare March 13, 2026 23:50
@logaretm

Copy link
Copy Markdown
Contributor Author

Looks like the skip isn't kicking in, I will check it locally with node 18

@wellwelwel

Copy link
Copy Markdown
Collaborator

Looks like the skip isn't kicking in, I will check it locally with node 18

It seems that the function itself is available, but not all of the necessary implementations are in place. I recommend something like:

if (version <= 18) { // from common.test.js
  skip('TracingChannel requires Node 19.9+ / 20+');
}

Comment thread test/integration/tracing-channel.test.mts
@logaretm

Copy link
Copy Markdown
Contributor Author

I see what's throwing it off, the specific node version in the tests does have tracingChannels but not hasSubscribers.

@wellwelwel should we strictly adhere to zero cost here and require hasSubcribers to be available for tracing to work? or are you okay that older node version won't have that optimization?

@wellwelwel

wellwelwel commented Mar 14, 2026 •

Copy link
Copy Markdown
Collaborator

should we strictly adhere to zero cost here and require hasSubcribers to be available for tracing to work? or are you okay that older node version won't have that optimization?

Since this would be a limitation of Node.js versions rather than of MySQL2 itself, I would choose to make the option unavailable for versions with "incomplete" support.

This could also be documented as this feature is available starting with Node.js v20, for example.

@logaretm

logaretm commented Mar 14, 2026 •

Copy link
Copy Markdown
Contributor Author

The overhead will be minimal anyways since if there are no subscribers then nothing really happens. Even propagation context stores would only run if only there are subscribers and then that's okay.

So it's an optimization that exists for newer node versions, but older node won't have it. But happy to stick to all or nothing approach here if you think that's best.

@wellwelwel

wellwelwel commented Mar 14, 2026 •

Copy link
Copy Markdown
Collaborator

But happy to stick to all or nothing approach here if you think that's best.

I'm fine with the current approach, as long as this is documented. Once all the tests pass, I'll take a closer look at the implementation 🙋🏻‍♂️

@wellwelwel

wellwelwel commented Mar 14, 2026 •

Copy link
Copy Markdown
Collaborator

For the documentation, I believe this feature should have its own section, similar to how we handle the use of Cloudflare Worker, for example:

Screenshot 2026-03-13 at 21 41 23

logaretm and others added 3 commits March 13, 2026 21:42
Node 18.x backported tracingChannel but the aggregated hasSubscribers
getter on TracingChannel is always undefined. Only skip tracing when
hasSubscribers is explicitly false — undefined means we can't tell, so
trace unconditionally (zero-cost optimization only on Node 20+).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tracePromise wraps operations in promises, which defers callbacks to
microtasks and breaks timing for existing tests. traceCallback calls
the function synchronously and wraps the callback to emit async
lifecycle events — no promise allocation, no timing change.

- query/execute callback mode: use traceCallback
- query/execute event-emitter mode: keep tracePromise (no callback)
- connect handshake: keep tracePromise (event-based)
- pool getConnection: use traceCallback
- shouldTrace: fall back to channel.start.hasSubscribers on Node 18
  where the aggregated getter is missing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@logaretm

Copy link
Copy Markdown
Contributor Author

@wellwelwel should the docs be in this PR as well, or in a separate PR?

@wellwelwel

wellwelwel commented Mar 14, 2026 •

Copy link
Copy Markdown
Collaborator

should the docs be in this PR as well, or in a separate PR?

Here, please 🙋🏻‍♂️

  • I usually use “Squash and Merge”, so all functionality + commits are grouped. I consider this an interesting way to keep the repository and contributions organized.

logaretm and others added 2 commits March 13, 2026 22:33
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for tracing channels

4 participants