Skip to content

mysql2 instrumentation is never applied when the app only imports 'mysql2/promise' (mysql2 >= 3.11.5) #5101

@Vahnra

Description

@Vahnra

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:
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()
  1. 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).
  2. 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)

  • 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:

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"

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions