跳到主要内容

Tracing Channels

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.

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
VersionChanges
v4.20.0
Added TracingChannel support for native APM instrumentation.

Channels

mysql2 emits events on four tracing channels:

ChannelFires whenContext type
mysql2:queryconnection.query() is calledQueryTraceContext
mysql2:executeconnection.execute() is called (prepared statements)ExecuteTraceContext
mysql2:connectA new connection handshake completes (or fails)ConnectTraceContext
mysql2:pool:connectpool.getConnection() is calledPoolConnectTraceContext

Each channel emits the standard TracingChannel lifecycle events: start, end, asyncStart, asyncEnd, and error.

Context Types

All context types are exported from the mysql2 package for TypeScript consumers.

QueryTraceContext

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)
}

ExecuteTraceContext

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)
}

ConnectTraceContext

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
}

PoolConnectTraceContext

interface PoolConnectTraceContext {
database: string; // Target database name
serverAddress: string; // Server host or socket path
serverPort: number | undefined; // Server port (undefined for unix sockets)
}

Basic Usage

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);
},
});

Tracing Queries

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"
});

Tracing Prepared Statements

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);
},
});

Tracing Connections

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);
},
});

Tracing Pool Connections

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);
},
});

Building Custom Spans

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
});

Zero-Cost When Unused

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.

Cleanup

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);

TypeScript

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() {},
});