mysql2-otel-instrumentation
v0.1.7
Published
OpenTelemetry instrumentation for mysql2 via Node.js diagnostic channels — no monkey-patching, no require-in-the-middle
Maintainers
Readme
mysql2-otel-instrumentation
OpenTelemetry instrumentation for mysql2 via Node.js diagnostic channels.
This package subscribes to mysql2's built-in TracingChannel events to produce OpenTelemetry spans and metrics. No monkey-patching, no require-in-the-middle, no import-in-the-middle.
Compatible with OpenTelemetry JS API and SDK 1.9+.
Why this package?
The OTel JS ecosystem is moving toward library-owned instrumentation (see @fastify/otel as precedent). This package replaces @opentelemetry/instrumentation-mysql2 with a cleaner architecture:
- Pure
diagnostics_channelsubscriber -- mysql2 >=3.20.0 emits structured lifecycle events natively - No RITM/IITM -- no module loading interception, no patching of internals
- Stable semantic conventions only -- no legacy attribute baggage
- Metrics included -- operation duration and connection timing histograms out of the box
- Connection visibility -- spans for connect and pool connect, not just queries
Migrating from
@opentelemetry/instrumentation-mysql2? See the Migration Guide.
Requirements
- Node.js >= 22
- mysql2 >= 3.20.0
- @opentelemetry/api >= 1.9.0
Installation
npm install mysql2-otel-instrumentationQuick Start
Note: You'll need the OpenTelemetry SDK packages alongside this instrumentation:
npm install @opentelemetry/sdk-trace-node @opentelemetry/instrumentation
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { MySQL2Instrumentation } from "mysql2-otel-instrumentation";
const provider = new NodeTracerProvider();
provider.register();
registerInstrumentations({
instrumentations: [new MySQL2Instrumentation()],
});That's it. Every connection.query(), connection.execute(), new Connection(), and pool.getConnection() call is now traced automatically.
With metrics
To also collect operation duration and connection timing metrics, configure a MeterProvider:
import {
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
const meterProvider = new MeterProvider({
readers: [
new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
}),
],
});If no MeterProvider is configured, metrics are silently no-op'd.
Configuration
Pass options to the constructor:
new MySQL2Instrumentation({
maskStatement: true,
requestHook(span, info) {
span.setAttribute("app.query.table", extractTable(info.query));
},
responseHook(span, info) {
span.setAttribute(
"app.query.rowCount",
Array.isArray(info.queryResults) ? info.queryResults.length : 0
);
},
});Options
| Option | Type | Default | Description |
| ------------------- | ----------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| requestHook | (span: Span, info: QueryRequestInfo) => void | undefined | Called on query/execute start. Add custom attributes from the query, parameters, or connection info. |
| responseHook | (span: Span, info: QueryResponseInfo) => void | undefined | Called on query/execute completion. Add custom attributes from query results. |
| maskStatement | boolean | false | If true, masks the SQL in db.query.text using maskStatementHook before setting the attribute. |
| maskStatementHook | (query: string) => string | Replaces integers and quoted strings with ? | Custom function to mask the SQL statement. Only used when maskStatement is true. |
| recordExceptions | boolean | true | If true, calls span.recordException(error) on error spans, producing an exception event with message and stack trace. |
| metrics | boolean | true | If false, skip emitting db.client.* histogram metrics entirely. No metric instruments are created and no records are made. |
Hook signatures
interface QueryRequestInfo {
query: string; // SQL text
values: unknown; // Bind parameter values
database: string; // Database name
serverAddress: string; // Host or socket path
serverPort: number | undefined; // Port (undefined for unix sockets)
}
interface QueryResponseInfo {
queryResults: unknown; // Result rows/info from mysql2
}Hooks are wrapped in try/catch -- a throwing hook will never break your application or the instrumentation.
Instrumented Operations
This package subscribes to four TracingChannel events emitted by mysql2:
| Channel | Span Name | When |
| --------------------- | ----------------------------------- | -------------------------------------------- |
| mysql2:query | SQL verb (e.g., SELECT, INSERT) | connection.query() |
| mysql2:execute | SQL verb (e.g., SELECT, INSERT) | connection.execute() (prepared statements) |
| mysql2:connect | mysql.connect | New connection handshake |
| mysql2:pool:connect | mysql.pool.connect | pool.getConnection() |
All spans are SpanKind.CLIENT.
Span Attributes
All spans include these base attributes:
| Attribute | Type | Description |
| ---------------- | -------- | -------------------------------------- |
| db.system.name | string | Always "mysql" |
| server.address | string | Hostname or unix socket path |
| server.port | number | Port number (omitted for unix sockets) |
| db.namespace | string | Database name (omitted if empty) |
Query and execute spans add:
| Attribute | Type | Description |
| --------------- | -------- | --------------------------------------------------- |
| db.query.text | string | The SQL statement (masked if maskStatement: true) |
Connect spans add:
| Attribute | Type | Description |
| --------- | -------- | -------------------------------- |
| db.user | string | Username used for the connection |
These follow the stable OpenTelemetry database semantic conventions (v1.33.0+).
Error Handling
When a query, execute, or connection operation fails:
- The span status is set to
ERRORwith the error message span.recordException(error)is called (unlessrecordExceptions: false), adding anexceptionevent with the error message and stack trace- The span is ended normally
Metrics
Two histogram metrics are emitted automatically:
| Metric | Unit | Description |
| ---------------------------------- | ---- | --------------------------------------------------------------- |
| db.client.operation.duration | s | Duration of query and execute operations |
| db.client.connection.create_time | s | Duration of connection establishment (connect and pool connect) |
Both use bucket boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] seconds.
Metrics carry the same base attributes as spans (db.system.name, server.address, server.port, db.namespace).
To disable metrics entirely (for trace-only setups or when no MeterProvider is configured), pass metrics: false:
new MySQL2Instrumentation({ metrics: false });When disabled, no histogram instruments are created and no record calls are made. The flag is read once at enable() time; to change it at runtime, call disable() then re-create the instrumentation.
SQL Masking
By default, db.query.text contains the SQL as mysql2 emits it -- typically the parameterized query template:
SELECT * FROM users WHERE id = ?Values are never interpolated into the attribute. To further mask the SQL:
new MySQL2Instrumentation({ maskStatement: true });
// "SELECT * FROM users WHERE id = 42" becomes "SELECT * FROM users WHERE id = ?"The default masking hook replaces integer literals and quoted strings with ?. Provide a custom hook for different behavior:
new MySQL2Instrumentation({
maskStatement: true,
maskStatementHook(query) {
return query.replace(/\b(password|secret|token)\s*=\s*\S+/gi, "$1=***");
},
});Examples
Adding custom attributes from results
new MySQL2Instrumentation({
responseHook(span, { queryResults }) {
if (Array.isArray(queryResults)) {
span.setAttribute("db.response.rowCount", queryResults.length);
}
},
});Adding query parameter count
new MySQL2Instrumentation({
requestHook(span, { values }) {
if (Array.isArray(values)) {
span.setAttribute("db.query.paramCount", values.length);
}
},
});Disabling exception recording
// Only set ERROR status, don't add exception events
new MySQL2Instrumentation({ recordExceptions: false });API
Exports
import {
MySQL2Instrumentation, // Main instrumentation class
INSTRUMENTATION_NAME, // "mysql2-otel-instrumentation"
} from "mysql2-otel-instrumentation";
import type {
MySQL2InstrumentationConfig, // Configuration options
QueryRequestInfo, // requestHook info parameter
QueryResponseInfo, // responseHook info parameter
} from "mysql2-otel-instrumentation";MySQL2Instrumentation
Extends InstrumentationBase from @opentelemetry/instrumentation.
const instrumentation = new MySQL2Instrumentation(config?);
instrumentation.enable(); // Subscribe to diagnostic channels (called automatically on construction)
instrumentation.disable(); // Unsubscribe from diagnostic channelsUseful Links
- mysql2 TracingChannel PR (#4178) -- the emitter side of this instrumentation
- OpenTelemetry Database Semantic Conventions
- Node.js TracingChannel API
- Migration Guide -- migrating from
@opentelemetry/instrumentation-mysql2
License
Apache-2.0. See LICENSE for the full text.
Copyright 2026 Vladimir Adamić.
