-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(server-utils): Migrate @opentelemetry/instrumentation-mysql2 to orchestrion
#22229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| import * as diagnosticsChannel from 'node:diagnostics_channel'; | ||
| import type { IntegrationFn, SpanAttributes } from '@sentry/core'; | ||
| import { | ||
| defineIntegration, | ||
| isObjectLike, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_OP, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, | ||
| SPAN_KIND, | ||
| startInactiveSpan, | ||
| waitForTracingChannelBinding, | ||
| } from '@sentry/core'; | ||
| import { subscribeMysql2DiagnosticChannels } from '../../mysql2/mysql2-dc-subscriber'; | ||
| import type { ChannelName } from '../../orchestrion/channels'; | ||
| import { CHANNELS } from '../../orchestrion/channels'; | ||
| import { bindTracingChannelToSpan } from '../../tracing-channel'; | ||
| import { | ||
| DB_NAME, | ||
| DB_STATEMENT, | ||
| DB_SYSTEM, | ||
| DB_USER, | ||
| NET_PEER_NAME, | ||
| NET_PEER_PORT, | ||
| } from '@sentry/conventions/attributes'; | ||
|
|
||
| const INTEGRATION_NAME = 'Mysql2' as const; | ||
| const ORIGIN = 'auto.db.orchestrion.mysql2'; | ||
| const DB_SYSTEM_VALUE_MYSQL = 'mysql'; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: I decided to drop |
||
|
|
||
| /** | ||
| * The shape orchestrion's transform attaches to the tracing-channel `context` object. Documented here | ||
| * rather than imported because orchestrion's runtime doesn't export it. | ||
| */ | ||
| interface Mysql2QueryChannelContext { | ||
| // The live args array passed to the wrapped `query`/`execute` call: `arguments[0]` is the SQL (a | ||
| // string, `Query`, or `{ sql, values }`), `arguments[1]` is the values array or a callback. | ||
| arguments: unknown[]; | ||
| self?: Mysql2Connection; | ||
| result?: unknown; | ||
| error?: unknown; | ||
| } | ||
|
|
||
| interface Mysql2ConnectionConfig { | ||
| host?: string; | ||
| port?: number | string; | ||
| database?: string; | ||
| user?: string; | ||
| // Pool connections nest the real config one level deeper. | ||
| connectionConfig?: Mysql2ConnectionConfig; | ||
| } | ||
|
|
||
| interface Mysql2Connection { | ||
| config?: Mysql2ConnectionConfig; | ||
| } | ||
|
|
||
| /** | ||
| * Orchestrion-driven mysql2 integration. | ||
| * | ||
| * Subscribes to: | ||
| * - the `orchestrion:mysql2:query`/`:execute` channels the code transform injects into mysql2's | ||
| * `query`/`execute` (mysql2 `< 3.20.0`), and | ||
| * - mysql2's native `node:diagnostics_channel` tracing channels (mysql2 `>= 3.20.0`), which the | ||
| * transform intentionally leaves alone. | ||
| * | ||
| * The two version ranges never overlap, so no query is double-counted. Requires the orchestrion | ||
| * runtime hook or bundler plugin to be active — wire that up via | ||
| * `experimentalUseDiagnosticsChannelInjection`. | ||
| */ | ||
| function instrumentMysql2(): void { | ||
| // mysql2 >= 3.20.0: native diagnostics_channel path (inert on older versions, which never publish). | ||
| subscribeMysql2DiagnosticChannels(diagnosticsChannel.tracingChannel); | ||
|
|
||
| // mysql2 < 3.20.0: orchestrion-injected channels (inert on >= 3.20.0, which we don't transform). | ||
| subscribeQueryChannel(CHANNELS.MYSQL2_QUERY); | ||
| subscribeQueryChannel(CHANNELS.MYSQL2_EXECUTE); | ||
| } | ||
|
|
||
| function subscribeQueryChannel(channelName: ChannelName): void { | ||
| bindTracingChannelToSpan( | ||
| diagnosticsChannel.tracingChannel<Mysql2QueryChannelContext>(channelName), | ||
| data => { | ||
| const statement = getQueryText(data.arguments); | ||
|
|
||
| return startInactiveSpan({ | ||
| name: statement ?? 'mysql2.query', | ||
| kind: SPAN_KIND.CLIENT, | ||
| attributes: { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', | ||
| // oxlint-disable-next-line typescript/no-deprecated | ||
| [DB_SYSTEM]: DB_SYSTEM_VALUE_MYSQL, | ||
| ...getConnectionAttributes(data.self?.config), | ||
| // oxlint-disable-next-line typescript/no-deprecated | ||
| [DB_STATEMENT]: statement || undefined, | ||
| }, | ||
| }); | ||
| }, | ||
| { requiresParentSpan: true }, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Render the `db.statement` from the wrapped call's first argument. | ||
| */ | ||
| function getQueryText(args: unknown[]): string | undefined { | ||
| return extractSql(args[0]); | ||
| } | ||
|
|
||
| function extractSql(firstArg: unknown): string | undefined { | ||
| if (typeof firstArg === 'string') { | ||
| return firstArg; | ||
| } | ||
| if (isObjectLike(firstArg) && 'sql' in firstArg) { | ||
| const sql = (firstArg as { sql?: unknown }).sql; | ||
| return typeof sql === 'string' ? sql : undefined; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| function getConnectionAttributes(config: Mysql2ConnectionConfig | undefined): SpanAttributes { | ||
| // Pool connections nest the real config under `.connectionConfig`; single connections expose it | ||
| // directly. Matches `@opentelemetry/instrumentation-mysql2`. | ||
| const { host, port, database, user } = config?.connectionConfig ?? config ?? {}; | ||
| const portNumber = typeof port === 'string' ? parseInt(port, 10) : port; | ||
| const portIsNumber = typeof portNumber === 'number' && !isNaN(portNumber); | ||
|
|
||
| return { | ||
| // oxlint-disable-next-line typescript/no-deprecated | ||
| [DB_NAME]: database || undefined, | ||
| [DB_USER]: user || undefined, | ||
| // oxlint-disable-next-line typescript/no-deprecated | ||
| [NET_PEER_NAME]: host || undefined, | ||
| // oxlint-disable-next-line typescript/no-deprecated | ||
| [NET_PEER_PORT]: portIsNumber ? portNumber : undefined, | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing JDBC connection string attributeMedium Severity The orchestrion Reviewed by Cursor Bugbot for commit be9e15d. Configure here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. on purpose |
||
| } | ||
|
|
||
| const _mysql2ChannelIntegration = (() => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This integration probably also needs to be added to Deno: https://github.com/getsentry/sentry-javascript/tree/develop/packages/deno/src/integrations (if mysql2 is expected to work on Deno)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And if this needs a Deno wrapper: this should have a guard (usually done with a closure like in redis) so it's not running twice.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would keep this in a separate/follow up PR IMHO, we do not need to do this at the same time :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if rather than one-off each Deno integration, there's a way to have the Deno SDK automatically slurp these all up. They're all pretty much identical, after all, and exported by server-utils. 🤔 |
||
| return { | ||
| name: INTEGRATION_NAME, | ||
| setupOnce() { | ||
| // `tracingChannel` is unavailable before Node 18.19 so do nothing in that case. | ||
| if (!diagnosticsChannel.tracingChannel) { | ||
| return; | ||
| } | ||
|
|
||
| waitForTracingChannelBinding(() => { | ||
| instrumentMysql2(); | ||
| }); | ||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
|
|
||
| /** | ||
| * Orchestrion-driven mysql2 integration. | ||
| * | ||
| * Adds Sentry tracing instrumentation for the [mysql2](https://www.npmjs.com/package/mysql2) library | ||
| * via diagnostics-channel injection. See {@link instrumentMysql2} for how the two mysql2 version | ||
| * ranges are covered. | ||
| * | ||
| * Known limitation vs. the OTel integration it replaces: the callback-less streaming form | ||
| * (`connection.query(sql).on('result', ...)`) is not traced — see the `mysql2` orchestrion config for | ||
| * why. The callback and promise forms (the common case) are fully instrumented. | ||
| */ | ||
| export const mysql2ChannelIntegration = defineIntegration(_mysql2ChannelIntegration); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,48 @@ | ||
| import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; | ||
|
|
||
| // TODO: Stub for the `mysql2` orchestrion integration (ports `@opentelemetry/instrumentation-mysql2`). | ||
| export const mysql2Config: InstrumentationConfig[] = []; | ||
| // Ports `@opentelemetry/instrumentation-mysql2` (which patches `query`/`execute` on the connection | ||
| // prototype) to orchestrion channel injection. | ||
| // | ||
| // Two file layouts across the supported range: | ||
| // - `< 3.11.5` → `class Connection` with `query`/`execute` in `lib/connection.js` | ||
| // - `>= 3.11.5` → the methods moved to `class BaseConnection` in `lib/base/connection.js` | ||
| // | ||
| // Gated to `< 3.20.0`: from 3.20.0 mysql2 publishes to `node:diagnostics_channel` natively, and | ||
| // that path is instrumented by `subscribeMysql2DiagnosticChannels` instead. Injecting there too | ||
| // would emit two spans per query. | ||
| // | ||
| // `Callback` traces the callback shapes: `query(sql, cb)`, `query(sql, values, cb)`, and — since the | ||
| // promise API funnels through the callback form — `await connection.query(sql)`. `execute` never | ||
| // delegates to `query` internally, so instrumenting both is safe. | ||
| // | ||
| // NOT `Auto`: `Auto`'s no-callback branch probes the return value for `.then`, but mysql2's callback- | ||
| // less `query(sql)` returns a `Query` whose `.then()` throws (mysql2's "use the promise wrapper" | ||
| // guard) — so `Auto` would crash streamed queries. `Callback` leaves that shape untouched (a rare, | ||
| // row-streaming use that consumes the emitter's events), the tradeoff being it isn't traced. | ||
| export const mysql2Config = [ | ||
| { | ||
| channelName: 'query', | ||
| module: { name: 'mysql2', versionRange: '>=1.4.2 <3.11.5', filePath: 'lib/connection.js' }, | ||
| functionQuery: { className: 'Connection', methodName: 'query', kind: 'Callback' }, | ||
| }, | ||
| { | ||
| channelName: 'execute', | ||
| module: { name: 'mysql2', versionRange: '>=1.4.2 <3.11.5', filePath: 'lib/connection.js' }, | ||
| functionQuery: { className: 'Connection', methodName: 'execute', kind: 'Callback' }, | ||
| }, | ||
| { | ||
| channelName: 'query', | ||
| module: { name: 'mysql2', versionRange: '>=3.11.5 <3.20.0', filePath: 'lib/base/connection.js' }, | ||
| functionQuery: { className: 'BaseConnection', methodName: 'query', kind: 'Callback' }, | ||
| }, | ||
| { | ||
| channelName: 'execute', | ||
| module: { name: 'mysql2', versionRange: '>=3.11.5 <3.20.0', filePath: 'lib/base/connection.js' }, | ||
| functionQuery: { className: 'BaseConnection', methodName: 'execute', kind: 'Callback' }, | ||
| }, | ||
| ] satisfies InstrumentationConfig[]; | ||
|
|
||
| export const mysql2Channels = {} as const; | ||
| export const mysql2Channels = { | ||
| MYSQL2_QUERY: 'orchestrion:mysql2:query', | ||
| MYSQL2_EXECUTE: 'orchestrion:mysql2:execute', | ||
| } as const; |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much more useful test 👍