website/docs/documentation/tracing-channels.mdx
import { History } from '@site/src/components/History';
mysql2 provides built-in instrumentation via Node.js TracingChannel from the node:diagnostics_channel module. This allows APM tools (OpenTelemetry, Datadog, Sentry, etc.) and custom instrumentation to subscribe to query, execute, connect, and pool lifecycle events without monkey-patching.
:::info Available in Node.js 20+
TracingChannel is available in Node.js 20+. On older versions where TracingChannel is not available, tracing is silently disabled with no overhead.
:::
<History records={[ { version: '4.20.0', changes: ['Added TracingChannel support for native APM instrumentation.'], }, ]} />
mysql2 emits events on four tracing channels:
| Channel | Fires when | Context type |
|---|---|---|
mysql2:query | connection.query() is called | QueryTraceContext |
mysql2:execute | connection.execute() is called (prepared statements) | ExecuteTraceContext |
mysql2:connect | A new connection handshake completes (or fails) | ConnectTraceContext |
mysql2:pool:connect | pool.getConnection() is called | PoolConnectTraceContext |
Each channel emits the standard TracingChannel lifecycle events: start, end, asyncStart, asyncEnd, and error.
All context types are exported from the mysql2 package for TypeScript consumers.
interface QueryTraceContext {
query: string; // The SQL query text
values: any; // Bind parameter values
database: string; // Target database name
serverAddress: string; // Server host or socket path
serverPort: number | undefined; // Server port (undefined for unix sockets)
}
interface ExecuteTraceContext {
query: string; // The prepared statement SQL
values: any; // Bind parameter values
database: string; // Target database name
serverAddress: string; // Server host or socket path
serverPort: number | undefined; // Server port (undefined for unix sockets)
}
interface ConnectTraceContext {
database: string; // Target database name
serverAddress: string; // Server host or socket path
serverPort: number | undefined; // Server port (undefined for unix sockets)
user: string; // MySQL user
}
interface PoolConnectTraceContext {
database: string; // Target database name
serverAddress: string; // Server host or socket path
serverPort: number | undefined; // Server port (undefined for unix sockets)
}
Subscribe to a channel using the standard node:diagnostics_channel API:
const diagnostics_channel = require('node:diagnostics_channel');
const queryChannel = diagnostics_channel.tracingChannel('mysql2:query');
queryChannel.subscribe({
start(ctx) {
console.log(`Query started: ${ctx.query}`);
console.log(` database: ${ctx.database}`);
console.log(` server: ${ctx.serverAddress}:${ctx.serverPort}`);
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
console.log(`Query completed: ${ctx.query}`);
},
error(ctx) {
console.log(`Query failed: ${ctx.query}`, ctx.error.message);
},
});
const diagnostics_channel = require('node:diagnostics_channel');
const mysql = require('mysql2');
// Subscribe to query events
const queryChannel = diagnostics_channel.tracingChannel('mysql2:query');
queryChannel.subscribe({
start(ctx) {
// Called synchronously when query() is invoked
// ctx.query - SQL text
// ctx.values - bind parameters
ctx.startTime = Date.now();
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
// Called when the query callback fires successfully
const duration = Date.now() - ctx.startTime;
console.log(`[${duration}ms] ${ctx.query}`);
},
error(ctx) {
// Called when the query fails
const duration = Date.now() - ctx.startTime;
console.error(`[${duration}ms] FAILED: ${ctx.query}`, ctx.error.message);
},
});
// Queries are now automatically traced
const connection = mysql.createConnection({ host: 'localhost', user: 'root' });
connection.query('SELECT 1 + 1 AS result', (err, rows) => {
// The subscriber above logs: "[2ms] SELECT 1 + 1 AS result"
});
const diagnostics_channel = require('node:diagnostics_channel');
const executeChannel = diagnostics_channel.tracingChannel('mysql2:execute');
executeChannel.subscribe({
start(ctx) {
console.log(`Execute: ${ctx.query} values=${JSON.stringify(ctx.values)}`);
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
console.log(`Execute completed: ${ctx.query}`);
},
error(ctx) {
console.error(`Execute failed: ${ctx.query}`, ctx.error.message);
},
});
const diagnostics_channel = require('node:diagnostics_channel');
const connectChannel = diagnostics_channel.tracingChannel('mysql2:connect');
connectChannel.subscribe({
start(ctx) {
console.log(
`Connecting to ${ctx.serverAddress}:${ctx.serverPort} as ${ctx.user}`
);
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
console.log(`Connected to ${ctx.serverAddress}:${ctx.serverPort}`);
},
error(ctx) {
console.error(`Connection failed:`, ctx.error.message);
},
});
const diagnostics_channel = require('node:diagnostics_channel');
const poolChannel = diagnostics_channel.tracingChannel('mysql2:pool:connect');
poolChannel.subscribe({
start(ctx) {
console.log(
`Pool acquiring connection to ${ctx.serverAddress}:${ctx.serverPort}`
);
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
console.log(`Pool connection acquired`);
},
error(ctx) {
console.error(`Pool connection failed:`, ctx.error.message);
},
});
A common use case is creating spans for APM tools. Here's a complete example using only the node:diagnostics_channel API to build a simple query logger with timing:
const diagnostics_channel = require('node:diagnostics_channel');
const mysql = require('mysql2');
// Track all active spans
const activeSpans = new WeakMap();
function subscribeChannel(name) {
const channel = diagnostics_channel.tracingChannel(name);
channel.subscribe({
start(ctx) {
activeSpans.set(ctx, {
channel: name,
query: ctx.query || null,
database: ctx.database,
server: `${ctx.serverAddress}:${ctx.serverPort}`,
startTime: process.hrtime.bigint(),
});
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
const span = activeSpans.get(ctx);
if (span) {
const duration = Number(process.hrtime.bigint() - span.startTime) / 1e6;
console.log(
`[${span.channel}] ${span.query || 'connect'} - ${duration.toFixed(1)}ms`
);
activeSpans.delete(ctx);
}
},
error(ctx) {
const span = activeSpans.get(ctx);
if (span) {
const duration = Number(process.hrtime.bigint() - span.startTime) / 1e6;
console.error(
`[${span.channel}] FAILED ${span.query || 'connect'} - ${duration.toFixed(1)}ms: ${ctx.error.message}`
);
activeSpans.delete(ctx);
}
},
});
}
// Subscribe to all channels
subscribeChannel('mysql2:query');
subscribeChannel('mysql2:execute');
subscribeChannel('mysql2:connect');
subscribeChannel('mysql2:pool:connect');
// All mysql2 operations are now traced
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'test',
});
connection.query('SELECT * FROM users WHERE id = ?', [1], (err, rows) => {
// Logs: [mysql2:query] SELECT * FROM users WHERE id = ? - 3.2ms
});
When no subscribers are attached to a channel, mysql2 skips the tracing code path entirely via hasSubscribers checks before any context objects are allocated. There is no performance overhead for applications that don't use tracing.
On Node.js versions where TracingChannel is not available (e.g. Node 18), tracing is silently disabled with zero overhead.
Always unsubscribe when you're done to prevent memory leaks:
const subscribers = {
start(ctx) {
/* ... */
},
end() {},
asyncStart() {},
asyncEnd(ctx) {
/* ... */
},
error(ctx) {
/* ... */
},
};
const channel = diagnostics_channel.tracingChannel('mysql2:query');
channel.subscribe(subscribers);
// Later, when done:
channel.unsubscribe(subscribers);
Context types are exported from the mysql2 package:
import type {
QueryTraceContext,
ExecuteTraceContext,
ConnectTraceContext,
PoolConnectTraceContext,
} from 'mysql2';
import diagnostics_channel from 'node:diagnostics_channel';
const channel = diagnostics_channel.tracingChannel('mysql2:query');
channel.subscribe({
start(ctx: QueryTraceContext) {
console.log(ctx.query, ctx.database);
},
end() {},
asyncStart() {},
asyncEnd() {},
error() {},
});