-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
ref(node): Streamline mysql2 instrumentation #21509
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
b9b8594
f03121f
33dea13
fd777c9
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 |
|---|---|---|
|
|
@@ -12,8 +12,10 @@ describe('mysql2 auto instrumentation', () => { | |
| expect.objectContaining({ | ||
| description: 'SELECT 1 + 1 AS solution', | ||
| op: 'db', | ||
| origin: 'auto.db.otel.mysql2', | ||
| data: expect.objectContaining({ | ||
| 'db.system': 'mysql', | ||
| 'db.statement': 'SELECT 1 + 1 AS solution', | ||
| 'net.peer.name': 'localhost', | ||
| 'net.peer.port': 3306, | ||
| 'db.user': 'root', | ||
|
|
@@ -22,13 +24,36 @@ describe('mysql2 auto instrumentation', () => { | |
| expect.objectContaining({ | ||
| description: 'SELECT NOW()', | ||
| op: 'db', | ||
| origin: 'auto.db.otel.mysql2', | ||
| data: expect.objectContaining({ | ||
| 'db.system': 'mysql', | ||
| 'db.statement': 'SELECT NOW()', | ||
| 'net.peer.name': 'localhost', | ||
| 'net.peer.port': 3306, | ||
| 'db.user': 'root', | ||
| }), | ||
| }), | ||
| // `execute` is instrumented the same way as `query` | ||
| expect.objectContaining({ | ||
| description: 'SELECT 42 AS answer', | ||
| op: 'db', | ||
| origin: 'auto.db.otel.mysql2', | ||
| data: expect.objectContaining({ | ||
| 'db.system': 'mysql', | ||
| 'db.statement': 'SELECT 42 AS answer', | ||
| }), | ||
| }), | ||
| // a failing query produces a span with an error status | ||
| expect.objectContaining({ | ||
| description: 'SELECT * FROM does_not_exist', | ||
| op: 'db', | ||
| status: 'internal_error', | ||
| origin: 'auto.db.otel.mysql2', | ||
|
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. Wrong error status assertionMedium Severity The failing-query expectation uses Reviewed by Cursor Bugbot for commit 33dea13. Configure here. |
||
| data: expect.objectContaining({ | ||
| 'db.system': 'mysql', | ||
| 'db.statement': 'SELECT * FROM does_not_exist', | ||
| }), | ||
| }), | ||
| ]), | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,75 +18,59 @@ | |||||
| * - 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 | ||||||
| */ | ||||||
| /* eslint-disable */ | ||||||
|
|
||||||
| import * as api from '@opentelemetry/api'; | ||||||
| import { SDK_VERSION } from '@sentry/core'; | ||||||
| import { | ||||||
| InstrumentationBase, | ||||||
| InstrumentationNodeModuleDefinition, | ||||||
| isWrapped, | ||||||
| safeExecuteInTheMiddle, | ||||||
| SemconvStability, | ||||||
| semconvStabilityFromStr, | ||||||
| } from '@opentelemetry/instrumentation'; | ||||||
|
|
||||||
| import { SpanKind } from '@opentelemetry/api'; | ||||||
| import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; | ||||||
| import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; | ||||||
| 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 { DB_SYSTEM_VALUE_MYSQL, ATTR_DB_STATEMENT, ATTR_DB_SYSTEM } from './semconv'; | ||||||
| import { addSqlCommenterComment } from '../../utils/sql-common'; | ||||||
| import type { Connection, Query, QueryOptions, QueryError, FieldPacket, FormatFunction } from './mysql2-types'; | ||||||
| import { MySQL2InstrumentationConfig } from './types'; | ||||||
| import type { Connection, FormatFunction, Query, QueryError, QueryOptions } from './mysql2-types'; | ||||||
| import { ATTR_DB_STATEMENT, ATTR_DB_SYSTEM, DB_SYSTEM_VALUE_MYSQL } from './semconv'; | ||||||
| import { getConnectionAttributes, getConnectionPrototypeToInstrument, getQueryText, getSpanName, once } from './utils'; | ||||||
| import { | ||||||
| ATTR_DB_QUERY_TEXT, | ||||||
| ATTR_DB_SYSTEM_NAME, | ||||||
| DB_SYSTEM_NAME_VALUE_MYSQL, | ||||||
| } from '@opentelemetry/semantic-conventions'; | ||||||
|
|
||||||
| const PACKAGE_NAME = '@sentry/instrumentation-mysql2'; | ||||||
| const ORIGIN = 'auto.db.otel.mysql2'; | ||||||
|
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. l: I don't think that we should add
Suggested change
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 think I'd opt to change all of these to something non-otel in v11, for now this indicates that it comes from otel instrumentation, which it still is (just our own)
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. Yea, my goal was not to change anything in terms of output. So I try to keep all attrs intact. |
||||||
|
|
||||||
| const supportedVersions = ['>=1.4.2 <4']; | ||||||
|
|
||||||
| export class MySQL2Instrumentation extends InstrumentationBase<MySQL2InstrumentationConfig> { | ||||||
| private _netSemconvStability!: SemconvStability; | ||||||
| private _dbSemconvStability!: SemconvStability; | ||||||
| // 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 }; | ||||||
|
|
||||||
| constructor(config: MySQL2InstrumentationConfig = {}) { | ||||||
| export class MySQL2Instrumentation extends InstrumentationBase<InstrumentationConfig> { | ||||||
| public constructor(config: InstrumentationConfig = {}) { | ||||||
| super(PACKAGE_NAME, SDK_VERSION, config); | ||||||
| this._setSemconvStabilityFromEnv(); | ||||||
| } | ||||||
|
|
||||||
| private _setSemconvStabilityFromEnv() { | ||||||
| this._netSemconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); | ||||||
| this._dbSemconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); | ||||||
| } | ||||||
|
|
||||||
| protected init() { | ||||||
| protected init(): InstrumentationNodeModuleDefinition[] { | ||||||
| let format: FormatFunction | undefined; | ||||||
| function setFormatFunction(moduleExports: any) { | ||||||
| function setFormatFunction(moduleExports: MySQL2Module): void { | ||||||
| if (!format && moduleExports.format) { | ||||||
| format = moduleExports.format; | ||||||
| } | ||||||
| } | ||||||
| const patch = (ConnectionPrototype: Connection) => { | ||||||
| const patch = (ConnectionPrototype: Connection): void => { | ||||||
| if (isWrapped(ConnectionPrototype.query)) { | ||||||
| this._unwrap(ConnectionPrototype, 'query'); | ||||||
| } | ||||||
| this._wrap(ConnectionPrototype, 'query', this._patchQuery(format, false) as any); | ||||||
| this._wrap(ConnectionPrototype, 'query', this._patchQuery(format) as any); | ||||||
| if (isWrapped(ConnectionPrototype.execute)) { | ||||||
| this._unwrap(ConnectionPrototype, 'execute'); | ||||||
| } | ||||||
| this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format, true) as any); | ||||||
| this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format) as any); | ||||||
| }; | ||||||
| const unpatch = (ConnectionPrototype: Connection) => { | ||||||
| const unpatch = (ConnectionPrototype: Connection): void => { | ||||||
| this._unwrap(ConnectionPrototype, 'query'); | ||||||
| this._unwrap(ConnectionPrototype, 'execute'); | ||||||
| }; | ||||||
| return [ | ||||||
| new InstrumentationNodeModuleDefinition( | ||||||
| 'mysql2', | ||||||
| supportedVersions, | ||||||
| (moduleExports: any) => { | ||||||
| (moduleExports: MySQL2Module) => { | ||||||
| setFormatFunction(moduleExports); | ||||||
| return moduleExports; | ||||||
| }, | ||||||
|
|
@@ -95,7 +79,7 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta | |||||
| new InstrumentationNodeModuleFile( | ||||||
| 'mysql2/promise.js', | ||||||
| supportedVersions, | ||||||
| (moduleExports: any) => { | ||||||
| (moduleExports: MySQL2Module) => { | ||||||
| setFormatFunction(moduleExports); | ||||||
| return moduleExports; | ||||||
| }, | ||||||
|
|
@@ -120,9 +104,9 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta | |||||
| ]; | ||||||
| } | ||||||
|
|
||||||
| private _patchQuery(format: FormatFunction | undefined, isPrepared: boolean) { | ||||||
| private _patchQuery(format: FormatFunction | undefined) { | ||||||
| const thisPlugin = this; | ||||||
| return (originalQuery: Function): Function => { | ||||||
| const thisPlugin = this; | ||||||
| return function query( | ||||||
| this: Connection, | ||||||
| query: string | Query | QueryOptions, | ||||||
|
|
@@ -135,61 +119,24 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta | |||||
| } else if (arguments[2]) { | ||||||
| values = [_valuesOrCallback]; | ||||||
| } | ||||||
| const { maskStatement, maskStatementHook, responseHook } = thisPlugin.getConfig(); | ||||||
|
|
||||||
|
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. Error span status mismatches testMedium Severity Failed queries set the span status Additional Locations (1)Reviewed by Cursor Bugbot for commit fd777c9. Configure here. |
||||||
| const attributes: api.Attributes = getConnectionAttributes( | ||||||
| this.config, | ||||||
| thisPlugin._dbSemconvStability, | ||||||
| thisPlugin._netSemconvStability, | ||||||
| ); | ||||||
| const dbQueryText = getQueryText(query, format, values, maskStatement, maskStatementHook); | ||||||
| if (thisPlugin._dbSemconvStability & SemconvStability.OLD) { | ||||||
| attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_MYSQL; | ||||||
| attributes[ATTR_DB_STATEMENT] = dbQueryText; | ||||||
| } | ||||||
| if (thisPlugin._dbSemconvStability & SemconvStability.STABLE) { | ||||||
| attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MYSQL; | ||||||
| attributes[ATTR_DB_QUERY_TEXT] = dbQueryText; | ||||||
| } | ||||||
|
|
||||||
| const span = thisPlugin.tracer.startSpan(getSpanName(query), { | ||||||
| kind: api.SpanKind.CLIENT, | ||||||
| const attributes: SpanAttributes = { | ||||||
| ...getConnectionAttributes(this.config), | ||||||
| [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MYSQL, | ||||||
| [ATTR_DB_STATEMENT]: getQueryText(query, format, values), | ||||||
| [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, | ||||||
| }; | ||||||
|
|
||||||
| const span = startInactiveSpan({ | ||||||
| name: getSpanName(query), | ||||||
| kind: SpanKind.CLIENT, | ||||||
|
logaretm marked this conversation as resolved.
|
||||||
| attributes, | ||||||
| }); | ||||||
|
Comment on lines
-151
to
158
Contributor
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. Bug: The Suggested FixTo correctly measure the full duration of the streaming query, the event listener should be bound to the Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews.
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. While this looks technically correct, we need to preserve the upstream instrumentation and track this individually later. |
||||||
|
|
||||||
| if (!isPrepared && thisPlugin.getConfig().addSqlCommenterCommentToQueries) { | ||||||
| arguments[0] = query = | ||||||
| typeof query === 'string' | ||||||
| ? addSqlCommenterComment(span, query) | ||||||
| : Object.assign(query, { | ||||||
| sql: addSqlCommenterComment(span, query.sql), | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| const endSpan = once((err?: any, results?: any) => { | ||||||
| const endSpan = once((err?: QueryError | null) => { | ||||||
| if (err) { | ||||||
| span.setStatus({ | ||||||
| code: api.SpanStatusCode.ERROR, | ||||||
| message: err.message, | ||||||
| }); | ||||||
| } else { | ||||||
| if (typeof responseHook === 'function') { | ||||||
| safeExecuteInTheMiddle( | ||||||
| () => { | ||||||
| responseHook(span, { | ||||||
| queryResults: results, | ||||||
| }); | ||||||
| }, | ||||||
| err => { | ||||||
| if (err) { | ||||||
| thisPlugin._diag.warn('Failed executing responseHook', err); | ||||||
| } | ||||||
| }, | ||||||
| true, | ||||||
| ); | ||||||
| } | ||||||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); | ||||||
| } | ||||||
|
|
||||||
| span.end(); | ||||||
| }); | ||||||
|
|
||||||
|
|
@@ -204,8 +151,8 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta | |||||
| .once('error', (err: any) => { | ||||||
| endSpan(err); | ||||||
| }) | ||||||
| .once('result', (results: any) => { | ||||||
| endSpan(undefined, results); | ||||||
| .once('result', () => { | ||||||
| endSpan(); | ||||||
| }); | ||||||
|
|
||||||
| return streamableQuery; | ||||||
|
|
@@ -222,11 +169,11 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta | |||||
| }; | ||||||
| } | ||||||
|
|
||||||
| private _patchCallbackQuery(endSpan: Function) { | ||||||
| private _patchCallbackQuery(endSpan: (err?: QueryError | null) => void) { | ||||||
| return (originalCallback: Function) => { | ||||||
| return function (err: QueryError | null, results?: any, _fields?: FieldPacket[]) { | ||||||
| endSpan(err, results); | ||||||
| return originalCallback(...arguments); | ||||||
| return function (...args: [err: QueryError | null, ...rest: unknown[]]) { | ||||||
| endSpan(args[0]); | ||||||
| return originalCallback(...args); | ||||||
| }; | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
This file was deleted.


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.
l: Is it necessary to add these (especially for all vendored integrations)?
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.
It was added in #21481 so depending on whichever gets merged first the other one will drop, this is a one time change only.