elastic / elastic/apm-agent-nodejs
mysql2 instrumentation is never applied when the app only imports 'mysql2/promise' (mysql2 >= 3.11.5)
- Dominant language
- JavaScript
- Stars
- 594
- Forks
- 244
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 16
Description
**Describe the bug**
Apps that only import `mysql2/promise` (the recommended promise API) get **zero MySQL spans** when using mysql2 >= 3.11.5. Error capture and HTTP spans work fine — only the mysql2 instrumentation is silently never applied.
Root cause: mysql2 3.11.5 resolved its internal circular dependencies (sidorares/node-mysql2#3081). Since that version, `promise.js` no longer requires `./index.js` (it builds on `lib/base/*` directly), so for promise-only apps the main `mysql2` module is **never loaded**. The agent only registers a require hook for `{ modPath: 'mysql2' }` (`lib/instrumentation/index.js`), so the hook never fires and `Connection.prototype.query/execute` are never wrapped.
Note: #4334 adapted the prototype patching to the new `BaseConnection` introduced by that same mysql2 refactor, but that code only runs when the `mysql2` hook fires — which never happens in this scenario. The existing test suite imports the main module, which is why this gap is invisible in CI.
Bisected: mysql2 **3.11.4 instruments fine, 3.11.5+ does not** (same agent, same script, only the mysql2 version changes).
**To Reproduce**
Steps to reproduce the behavior:
1. `npm i elastic-apm-node mysql2` (any mysql2 >= 3.11.5; verified up to 3.22.4)
2. Run the following script against any reachable MySQL server:
```js
const apm = require('elastic-apm-node').start({
serviceName: 'repro',
disableSend: true,
centralConfig: false,
cloudProvider: 'none',
metricsInterval: '0s',
})
// Promise API only — like most modern apps. Never requires the main 'mysql2' module.
const mysql = require('mysql2/promise')
// Proof the instrumentation target was never loaded:
console.log('mysql2/index.js in require.cache:',
Object.keys(require.cache).some((k) => k.endsWith('/mysql2/index.js'))) // -> false
async function main() {
const pool = mysql.createPool({ host: '127.0.0.1', user: 'root' })
const tx = apm.startTransaction('t', 'request')
await pool.query('SELECT 1') // no db span is created
tx.end()
await pool.end()
}
main()
```
3. Observe that no `db.mysql` span is created for the query (e.g. via a mock APM server intake, or simply the absence of `intercepted call to mysql2.query` in trace logs).
4. Add a side-effect `require('mysql2')` before step 2's import → spans are created. Same result by downgrading to mysql2 3.11.4.
**Expected behavior**
`pool.query()` / `pool.execute()` through `mysql2/promise` produce `db.mysql` spans, as they do with mysql2 <= 3.11.4 or whenever the main `mysql2` module happens to be loaded by other code.
Suggested fix: also register `{ modPath: 'mysql2/promise' }`, with a handler that simply loads the main `mysql2` module (triggering the existing patcher) and returns the promise exports unchanged. Both APIs share the same `BaseConnection.prototype`, and the existing patcher only runs once per module-cache entry, so no double-patching occurs.
**Environment (please complete the following information)**
- OS: Linux (production) and macOS 15 (reproduced on both)
- Node.js version: 20.19.2 (CJS and ESM apps both affected)
- APM Server version: 8.6.1 (irrelevant — reproducible with `disableSend: true`, this is agent-side)
- Agent version: 4.15.0 (latest; also reproduced on 4.11.0)
**How are you starting the agent? (please tick one of the boxes)**
- [x] Calling `agent.start()` directly (e.g. `require('elastic-apm-node').start(...)`)
- [ ] Requiring `elastic-apm-node/start` from within the source code
- [ ] Starting node with `-r elastic-apm-node/start`
**Additional context**
Impact: this is a *silent* observability regression. Any app importing only `mysql2/promise` loses all SQL spans the day mysql2 crosses 3.11.5 via a routine dependency bump — no error, no warning. We confirmed five production services affected this way.
Workaround for affected users: add a side-effect import of the main module before the promise import:
```js
import 'mysql2' // forces the agent's require hook to fire; shared prototypes make the promise API instrumented too
import mysql from 'mysql2/promise'
```
Agent config options:
Click to expand
```
serviceName: ''
secretToken: process.env.ES_ADDON_APM_AUTH_TOKEN
environment: process.env.NODE_ENV
serverUrl: 'https://' + process.env.ES_ADDON_APM_HOST
```
`package.json` dependencies:
Click to expand
```
"elastic-apm-node": "^4.5.4",
"mysql2": "^3.13.0"
```
Contributor guide
Assessment
This issue has not been assessed yet.