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:
npm i elastic-apm-node mysql2 (any mysql2 >= 3.11.5; verified up to 3.22.4)
- Run the following script against any reachable MySQL server:
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()
- 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).
- 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)
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:
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: '<service>'
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"
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.jsno longer requires./index.js(it builds onlib/base/*directly), so for promise-only apps the mainmysql2module is never loaded. The agent only registers a require hook for{ modPath: 'mysql2' }(lib/instrumentation/index.js), so the hook never fires andConnection.prototype.query/executeare never wrapped.Note: #4334 adapted the prototype patching to the new
BaseConnectionintroduced by that same mysql2 refactor, but that code only runs when themysql2hook 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:
npm i elastic-apm-node mysql2(any mysql2 >= 3.11.5; verified up to 3.22.4)db.mysqlspan is created for the query (e.g. via a mock APM server intake, or simply the absence ofintercepted call to mysql2.queryin trace logs).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()throughmysql2/promiseproducedb.mysqlspans, as they do with mysql2 <= 3.11.4 or whenever the mainmysql2module happens to be loaded by other code.Suggested fix: also register
{ modPath: 'mysql2/promise' }, with a handler that simply loads the mainmysql2module (triggering the existing patcher) and returns the promise exports unchanged. Both APIs share the sameBaseConnection.prototype, and the existing patcher only runs once per module-cache entry, so no double-patching occurs.Environment (please complete the following information)
disableSend: true, this is agent-side)How are you starting the agent? (please tick one of the boxes)
agent.start()directly (e.g.require('elastic-apm-node').start(...))elastic-apm-node/startfrom within the source code-r elastic-apm-node/startAdditional context
Impact: this is a silent observability regression. Any app importing only
mysql2/promiseloses 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:
Agent config options:
Click to expand
package.jsondependencies:Click to expand