Skip to content

feat: add TracingChannel support for native APM instrumentation#4178

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

feat: add TracingChannel support for native APM instrumentation#4178
wellwelwel merged 10 commits intosidorares:masterfrom
logaretm:feat/tracing-channel-support

Conversation

@logaretm
Copy link
Contributor

@logaretm logaretm commented Mar 13, 2026

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
Copy link
Collaborator

wellwelwel commented Mar 13, 2026

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
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
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
Copy link
Collaborator

wellwelwel commented Mar 13, 2026

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
Copy link

codecov bot commented Mar 13, 2026

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>
…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
Contributor Author

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

@wellwelwel
Copy link
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+');
}

@logaretm
Copy link
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
Copy link
Collaborator

wellwelwel commented Mar 14, 2026

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
Copy link
Contributor Author

logaretm commented Mar 14, 2026

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
Copy link
Collaborator

wellwelwel commented Mar 14, 2026

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
Copy link
Collaborator

wellwelwel commented Mar 14, 2026

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
Contributor Author

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

@wellwelwel
Copy link
Collaborator

wellwelwel commented Mar 14, 2026

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>
@logaretm
Copy link
Contributor Author

(e.g., logic duplication between prepare + execute and shouldTrace checks that can be simplified).

Yea I think I see what you want to do there! Sounds Good 🙌

There is a question about the approach indirectly bringing a breaking change by renaming the Pool connection that has the original behavior from getConnection to _getConnection....

I didn't expect that, thanks for pointing it out! I thought just replacing it with a wrapper would preserve the API.

can I CC you on PRs I open related to these non-blocking issues regarding the current contribution?

Yes ofc, LMK if you need any help!

@wellwelwel
Copy link
Collaborator

After refactoring the potential breaking change I mentioned about _getConnection, I ended up noticing a way to solve #3273 that had been open for over a year.

I finished all the minor fixes and will probably release the new version on Monday:

Thanks again, @logaretm 🙋🏻‍♂️

@logaretm
Copy link
Contributor Author

Many thanks @wellwelwel ❤️ this goes a long way into reducing the ecosystem reliance on import-in-the-middle and hopefully APMs won't need it anymore if more libraries follow suit.

Do you mind using the work done here as an example I can point out for similar libraries?

@logaretm logaretm deleted the feat/tracing-channel-support branch March 15, 2026 02:53
@wellwelwel
Copy link
Collaborator

Do you mind using the work done here as an example I can point out for similar libraries?

Please feel free 🤝

@nymkappa
Copy link

nymkappa commented Mar 16, 2026

Did this PR breaks typescript types? I'm trying to bump our mysql2 dep from 3.19.0 to 3.20.0 but it does not build anymore.

node_modules/mysql2/typings/mysql/lib/Tracing.d.ts:1:15 - error TS2305: Module '"node:diagnostics_channel"' has no exported member 'TracingChannel'.

1 import type { TracingChannel } from 'node:diagnostics_channel';
                ~~~~~~~~~~~~~~


Found 1 error in node_modules/mysql2/typings/mysql/lib/Tracing.d.ts:1

@wellwelwel
Copy link
Collaborator

Did this PR breaks typescript types? I'm trying to bump our mysql2 dep from 3.19.0 to 3.20.0 but it does not build anymore.

@nymkappa, which version of @types/node are you using?

@nymkappa
Copy link

which version of @types/node are you using?

We don't explicitly require @types/node but I can see in our package-lock that it's set to 20.5.0.

    "@types/node": {
      "version": "20.5.0",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz",
      "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q=="
    },

@nymkappa
Copy link

Adding the latest version using npm install --save @types/node also breaks the compilation with dozens of errors

Found 80 errors in 25 files.

Errors  Files
     2  node_modules/@types/node/child_process.d.ts:323
     2  node_modules/@types/node/dgram.d.ts:558
     1  node_modules/@types/node/events.d.ts:581
     9  node_modules/@types/node/fs.d.ts:330
     5  node_modules/@types/node/fs/promises.d.ts:496
     1  node_modules/@types/node/inspector.d.ts:49
     2  node_modules/@types/node/net.d.ts:617
     2  node_modules/@types/node/perf_hooks.d.ts:461
     6  node_modules/@types/node/quic.d.ts:427
     3  node_modules/@types/node/readline.d.ts:63
     5  node_modules/@types/node/sqlite.d.ts:263
     5  node_modules/@types/node/stream.d.ts:678
     1  node_modules/@types/node/stream/promises.d.ts:202
     2  node_modules/@types/node/test.d.ts:2222
     6  node_modules/@types/node/timers.d.ts:34
     1  node_modules/@types/node/ts5.6/index.d.ts:29
     8  node_modules/@types/node/v8.d.ts:421
     2  node_modules/@types/node/web-globals/streams.d.ts:40
     2  node_modules/@types/node/worker_threads.d.ts:394
     6  node_modules/mysql2/promise.d.ts:68
     2  node_modules/mysql2/typings/mysql/lib/Connection.d.ts:397
     2  node_modules/mysql2/typings/mysql/lib/Pool.d.ts:56
     2  node_modules/mysql2/typings/mysql/lib/PoolCluster.d.ts:59
     2  node_modules/mysql2/typings/mysql/lib/PoolConnecti

@logaretm
Copy link
Contributor Author

logaretm commented Mar 16, 2026

@nymkappa @wellwelwel I think it is available in types starting from v20.13.0

@nymkappa
Copy link

nymkappa commented Mar 16, 2026

@nymkappa @wellwelwel I think it is available in types starting from v20.13.0

I bumped my local nodejs version to v24.14.0 and installed @types/node@24.12.0 and it also does not build

Found 69 errors in 22 files.

Errors  Files
     2  node_modules/@types/node/child_process.d.ts:310
     2  node_modules/@types/node/dgram.d.ts:595
     1  node_modules/@types/node/events.d.ts:403
     9  node_modules/@types/node/fs.d.ts:331
     5  node_modules/@types/node/fs/promises.d.ts:497
     1  node_modules/@types/node/inspector.d.ts:49
     2  node_modules/@types/node/net.d.ts:739
     2  node_modules/@types/node/perf_hooks.d.ts:798
     3  node_modules/@types/node/readline.d.ts:54
     5  node_modules/@types/node/sqlite.d.ts:236
     4  node_modules/@types/node/stream.d.ts:628
     2  node_modules/@types/node/test.d.ts:2206
     6  node_modules/@types/node/timers.d.ts:34
     1  node_modules/@types/node/ts5.6/index.d.ts:29
     6  node_modules/@types/node/v8.d.ts:417
     2  node_modules/@types/node/web-globals/streams.d.ts:10
     2  node_modules/@types/node/worker_threads.d.ts:556
     6  node_modules/mysql2/promise.d.ts:68
     2  node_modules/mysql2/typings/mysql/lib/Connection.d.ts:397
     2  node_modules/mysql2/typings/mysql/lib/Pool.d.ts:56
     2  node_modules/mysql2/typings/mysql/lib/PoolCluster.d.ts:59
     2  node_modules/mysql2/typings/mysql/lib/PoolConnection.d.ts:7

Same result if I stay on mysql2@3.19.0 using @types/node@24.12.0

@logaretm
Copy link
Contributor Author

I think the tracing channel import is gone since the errors got reduced. Which version of typescript are you using? there is a known conflict in buffer types related to Uint8Array being a generic in latter TS versions.

@nymkappa
Copy link

I think the tracing channel import is gone since the errors got reduced. Which version of typescript are you using? there is a known conflict in buffer types related to Uint8Array being a generic in latter TS versions.

"typescript": "~4.9.3"

@wellwelwel
Copy link
Collaborator

"typescript": "~4.9.3"

@nymkappa, can you create a basic test case that reproduces the error?

Everything is working fine for me. I'm testing with a @types/node version before the diagnostics_channel:

Screenshot 2026-03-17 at 04 42 10

@nymkappa
Copy link

nymkappa commented Mar 17, 2026

"typescript": "~4.9.3"

@nymkappa, can you create a basic test case that reproduces the error?

Everything is working fine for me. I'm testing with a @types/node version before the diagnostics_channel:

It works with @types/node@18.19.130, but not with more recent versions. Are you saying that you're not supporting more recent versions of nodejs (and corresponding types definitions?)

@wellwelwel
Copy link
Collaborator

Are you saying that you're not supporting more recent versions of nodejs (and corresponding types definitions?)

I'm just trying to reproduce the error you reported.

It seems the problem lies with the TypeScript version itself, not with @types/node:

{
  "devDependencies": {
    "@types/node": "^25.5.0",
    "typescript": "~5.2.2"
  },
  "dependencies": {
    "mysql2": "^3.20.0"
  }
}

Before TypeScript v5.2.2 it doesn't recognize the [Symbol.dispose]().

@wellwelwel
Copy link
Collaborator

wellwelwel commented Mar 17, 2026

It works with @types/node@18.19.130, but not with more recent versions. Are you saying that you're not supporting more recent versions of nodejs (and corresponding types definitions?)

Testing in latest @types/node (v25.5.0), latest TypeScript (v5.9.3), and latest MySQL2 (v3.20.0), everything works fine too:

Screenshot 2026-03-17 at 05 06 50
  • I also couldn't reproduce any errors related to this PR.

@nymkappa
Copy link

nymkappa commented Mar 17, 2026

Thanks, I was able to get it working with node v24.14.0, "@types/node": "^24.12.0" and latest typescript 5.9.3.

Okay so this means mysql2 is not compatible with older typescript version then? Starting from 3.20.0, or am I still missing something.

@wellwelwel
Copy link
Collaborator

Thanks, I was able to get it working with node v24.14.0, "@types/node": "^24.12.0" and latest typescript 5.9.3.

Okay so this means mysql2 is not compatible with older typescript version then? Starting from 3.20.0, or am I still missing something.

Within the limits of what I was able to test, it seems more like a conflict between TypeScript and @types/node versions than MySQL2 itself. An example of this is the fact that it worked when reverting the @types/node version to 18 with the old TypeScript version.

I think that if the problem were with MySQL2 itself, no matter which @types/node version, it should break, but it would be interesting to have a way to reproduce it to be sure of that.​​​​​​​​​​​​​​​​

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