diff --git a/packages/node/src/integrations/tracing/mysql2/index.ts b/packages/node/src/integrations/tracing/mysql2/index.ts deleted file mode 100644 index f2c4489e2230..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MySQL2Instrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Mysql2' as const; - -export const instrumentMysql2 = generateInstrumentOnce(INTEGRATION_NAME, () => new MySQL2Instrumentation()); diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts deleted file mode 100644 index c7c0e7e73746..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - * - Types from 'mysql2' inlined as simplified interfaces - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { DB_STATEMENT, DB_SYSTEM, SENTRY_KIND } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import type { Connection, FormatFunction, Query, QueryError, QueryOptions } from './mysql2-types'; -import { DB_SYSTEM_VALUE_MYSQL } from './semconv'; -import { getConnectionAttributes, getConnectionPrototypeToInstrument, getQueryText, getSpanName, once } from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-mysql2'; -const ORIGIN = 'auto.db.otel.mysql2'; - -// mysql2 >= 3.20.0 publishes via diagnostics_channel and is instrumented by -// `subscribeMysql2DiagnosticChannels` instead, so this IITM patcher must not -// overlap it — otherwise every query would emit two mysql2 spans. -const supportedVersions = ['>=1.4.2 <3.20.0']; - -// The raw imported `mysql2` module exposes the `format` helper used to render -// parameterized queries. Typed shallowly since it is only read internally. -type MySQL2Module = { format?: FormatFunction; [key: string]: unknown }; - -export class MySQL2Instrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition[] { - let format: FormatFunction | undefined; - function setFormatFunction(moduleExports: MySQL2Module): void { - if (!format && moduleExports.format) { - format = moduleExports.format; - } - } - const patch = (ConnectionPrototype: Connection): void => { - if (isWrapped(ConnectionPrototype.query)) { - this._unwrap(ConnectionPrototype, 'query'); - } - this._wrap(ConnectionPrototype, 'query', this._patchQuery(format) as any); - if (isWrapped(ConnectionPrototype.execute)) { - this._unwrap(ConnectionPrototype, 'execute'); - } - this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format) as any); - }; - const unpatch = (ConnectionPrototype: Connection): void => { - this._unwrap(ConnectionPrototype, 'query'); - this._unwrap(ConnectionPrototype, 'execute'); - }; - return [ - new InstrumentationNodeModuleDefinition( - 'mysql2', - supportedVersions, - (moduleExports: MySQL2Module) => { - setFormatFunction(moduleExports); - return moduleExports; - }, - () => {}, - [ - new InstrumentationNodeModuleFile( - 'mysql2/promise.js', - supportedVersions, - (moduleExports: MySQL2Module) => { - setFormatFunction(moduleExports); - return moduleExports; - }, - () => {}, - ), - new InstrumentationNodeModuleFile( - 'mysql2/lib/connection.js', - supportedVersions, - (moduleExports: any) => { - const ConnectionPrototype: Connection = getConnectionPrototypeToInstrument(moduleExports); - patch(ConnectionPrototype); - return moduleExports; - }, - (moduleExports: any) => { - if (moduleExports === undefined) return; - const ConnectionPrototype: Connection = getConnectionPrototypeToInstrument(moduleExports); - unpatch(ConnectionPrototype); - }, - ), - ], - ), - ]; - } - - private _patchQuery(format: FormatFunction | undefined) { - const thisPlugin = this; - return (originalQuery: Function): Function => { - return function query( - this: Connection, - query: string | Query | QueryOptions, - _valuesOrCallback?: unknown[] | Function, - _callback?: Function, - ) { - let values; - if (Array.isArray(_valuesOrCallback)) { - values = _valuesOrCallback; - } else if (arguments[2]) { - values = [_valuesOrCallback]; - } - - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - ...getConnectionAttributes(this.config), - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_MYSQL, - // oxlint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: getQueryText(query, format, values), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; - - const span = startInactiveSpan({ - name: getSpanName(query), - attributes, - }); - - const endSpan = once((err?: QueryError | null) => { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - }); - - if (arguments.length === 1) { - if (typeof (query as any).onResult === 'function') { - thisPlugin._wrap(query as any, 'onResult', thisPlugin._patchCallbackQuery(endSpan)); - } - - const streamableQuery: Query = originalQuery.apply(this, arguments); - - streamableQuery - .once('error', (err: any) => { - endSpan(err); - }) - .once('result', () => { - endSpan(); - }); - - return streamableQuery; - } - - if (typeof arguments[1] === 'function') { - thisPlugin._wrap(arguments, 1, thisPlugin._patchCallbackQuery(endSpan)); - } else if (typeof arguments[2] === 'function') { - thisPlugin._wrap(arguments, 2, thisPlugin._patchCallbackQuery(endSpan)); - } - - return originalQuery.apply(this, arguments); - }; - }; - } - - private _patchCallbackQuery(endSpan: (err?: QueryError | null) => void) { - return (originalCallback: Function) => { - return function (...args: [err: QueryError | null, ...rest: unknown[]]) { - endSpan(args[0]); - return originalCallback(...args); - }; - }; - } -} diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts b/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts deleted file mode 100644 index 5b3f105eb997..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Simplified type definitions from the 'mysql2' package. - * Only the members actually used by the instrumentation are included. - */ - -export interface Connection { - config: ConnectionConfig; - query: (...args: any[]) => Query; - execute: (...args: any[]) => Query; - [key: string]: any; -} - -export interface ConnectionConfig { - host?: string; - port?: number; - database?: string; - user?: string; - connectionConfig?: ConnectionConfig; - [key: string]: any; -} - -export interface Query { - sql: string; - onResult?: (...args: any[]) => any; - once(event: string, callback: (...args: any[]) => void): Query; - [key: string]: any; -} - -export interface QueryOptions { - sql: string; - values?: any | any[] | { [param: string]: any }; - [key: string]: any; -} - -export interface QueryError extends Error { - code?: string; - errno?: number; - sqlState?: string; - sqlMessage?: string; - [key: string]: any; -} - -export interface FieldPacket { - [key: string]: any; -} - -export type FormatFunction = (sql: string, values?: any[], stringifyObjects?: boolean, timeZone?: string) => string; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts b/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts deleted file mode 100644 index 2dbf2793b106..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - */ - -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; -export const DB_SYSTEM_VALUE_MYSQL = 'mysql' as const; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts b/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts deleted file mode 100644 index 28636a9fee4c..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - * - Types from 'mysql2' inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { DB_NAME, DB_USER, NET_PEER_NAME, NET_PEER_PORT } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import type { FormatFunction } from './mysql2-types'; -import { ATTR_DB_CONNECTION_STRING } from './semconv'; - -interface QueryOptions { - sql: string; - values?: any | any[] | { [param: string]: any }; -} - -interface Query { - sql: string; -} - -interface Config { - host?: string; - port?: number; - database?: string; - user?: string; - connectionConfig?: Config; -} - -export function getConnectionAttributes(config: Config): SpanAttributes { - const { host, port, database, user } = getConfig(config); - - const attrs: SpanAttributes = { - [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, port, database), - // oxlint-disable-next-line typescript/no-deprecated - [DB_NAME]: database, - [DB_USER]: user, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: host, - }; - - const portNumber = parseInt(port, 10); - if (!isNaN(portNumber)) { - // oxlint-disable-next-line typescript/no-deprecated - attrs[NET_PEER_PORT] = portNumber; - } - - return attrs; -} - -function getConfig(config: any) { - const { host, port, database, user } = config?.connectionConfig || config || {}; - return { host, port, database, user }; -} - -function getJDBCString(host: string | undefined, port: number | undefined, database: string | undefined) { - let jdbcString = `jdbc:mysql://${host || 'localhost'}`; - - if (typeof port === 'number') { - jdbcString += `:${port}`; - } - - if (typeof database === 'string') { - jdbcString += `/${database}`; - } - - return jdbcString; -} - -export function getQueryText(query: string | Query | QueryOptions, format?: FormatFunction, values?: any[]): string { - const [querySql, queryValues] = - typeof query === 'string' ? [query, values] : [query.sql, hasValues(query) ? values || query.values : values]; - try { - if (format && queryValues) { - return format(querySql, queryValues); - } else { - return querySql; - } - } catch { - return 'Could not determine the query due to an error in formatting'; - } -} - -function hasValues(obj: Query | QueryOptions): obj is QueryOptions { - return 'values' in obj; -} - -export function getSpanName(query: string | Query | QueryOptions): string { - const rawQuery = typeof query === 'object' ? query.sql : query; - const firstSpace = rawQuery?.indexOf(' '); - if (typeof firstSpace === 'number' && firstSpace !== -1) { - return rawQuery?.substring(0, firstSpace); - } - return rawQuery; -} - -export const once = (fn: Function) => { - let called = false; - return (...args: unknown[]) => { - if (called) return; - called = true; - return fn(...args); - }; -}; - -export function getConnectionPrototypeToInstrument(connection: any) { - const connectionPrototype = connection.prototype; - const basePrototype = Object.getPrototypeOf(connectionPrototype); - - if (typeof basePrototype?.query === 'function' && typeof basePrototype?.execute === 'function') { - return basePrototype; - } - - return connectionPrototype; -} diff --git a/packages/node/test/integrations/tracing/mysql2.test.ts b/packages/node/test/integrations/tracing/mysql2.test.ts deleted file mode 100644 index 5d3d2bc42f58..000000000000 --- a/packages/node/test/integrations/tracing/mysql2.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getConnectionPrototypeToInstrument } from '../../../src/integrations/tracing/mysql2/vendored/utils'; - -// The instrumentation patches `query`/`execute` on whichever prototype actually owns them. -// mysql2's layout differs across major versions: older versions define them directly on -// `Connection.prototype`, newer versions inherit them from a base class. This is the only -// version-sensitive logic in the instrumentation, so it's covered here as a pure unit. -// The end-to-end span behavior is covered by the real-package integration suite. -describe('getConnectionPrototypeToInstrument', () => { - it('returns the connection prototype when query/execute live on it directly', () => { - class Connection {} - (Connection.prototype as any).query = (): void => {}; - (Connection.prototype as any).execute = (): void => {}; - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Connection.prototype); - }); - - it('returns the base prototype when query/execute are inherited from a base class', () => { - class Base {} - (Base.prototype as any).query = (): void => {}; - (Base.prototype as any).execute = (): void => {}; - class Connection extends Base {} - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Base.prototype); - }); - - it('falls back to the connection prototype when the base lacks query/execute', () => { - class Base {} - (Base.prototype as any).query = (): void => {}; - // base only has `query`, not `execute` -> not a valid instrumentation target - class Connection extends Base {} - (Connection.prototype as any).query = (): void => {}; - (Connection.prototype as any).execute = (): void => {}; - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Connection.prototype); - }); -});