diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 9cbf98e1bdd7..8fe892e64ad9 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -152,6 +152,12 @@ "typescript/no-explicit-any": "off" } }, + { + "files": ["**/integrations/tracing/knex/vendored/**/*.ts"], + "rules": { + "typescript/no-explicit-any": "off" + } + }, { "files": ["**/scenarios/**", "**/rollup-utils/**"], "rules": { diff --git a/packages/node/package.json b/packages/node/package.json index 9f7405b0ee91..c8126e098268 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -75,7 +75,6 @@ "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", - "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", diff --git a/packages/node/src/integrations/tracing/knex.ts b/packages/node/src/integrations/tracing/knex/index.ts similarity index 95% rename from packages/node/src/integrations/tracing/knex.ts rename to packages/node/src/integrations/tracing/knex/index.ts index 433e10e7ee70..9698da0813fe 100644 --- a/packages/node/src/integrations/tracing/knex.ts +++ b/packages/node/src/integrations/tracing/knex/index.ts @@ -1,4 +1,4 @@ -import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex'; +import { KnexInstrumentation } from './vendored/instrumentation'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core'; diff --git a/packages/node/src/integrations/tracing/knex/vendored/constants.ts b/packages/node/src/integrations/tracing/knex/vendored/constants.ts new file mode 100644 index 000000000000..e7e8655c1810 --- /dev/null +++ b/packages/node/src/integrations/tracing/knex/vendored/constants.ts @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex + * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 + */ +/* eslint-disable */ + +export const MODULE_NAME = 'knex'; +export const SUPPORTED_VERSIONS = [ + // use "lib/execution" for runner.js, "lib" for client.js as basepath, latest tested 0.95.6 + '>=0.22.0 <4', + // use "lib" as basepath + '>=0.10.0 <0.18.0', + '>=0.19.0 <0.22.0', + // use "src" as basepath + '>=0.18.0 <0.19.0', +]; diff --git a/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts new file mode 100644 index 000000000000..4c646b23d820 --- /dev/null +++ b/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts @@ -0,0 +1,232 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex + * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 + * - Minor TypeScript strictness adjustments for this repository's compiler settings + */ +/* eslint-disable */ + +import * as api from '@opentelemetry/api'; +import { SDK_VERSION } from '@sentry/core'; +import * as constants from './constants'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + SemconvStability, + semconvStabilityFromStr, +} from '@opentelemetry/instrumentation'; +import * as utils from './utils'; +import { KnexInstrumentationConfig } from './types'; +import { + ATTR_DB_COLLECTION_NAME, + ATTR_DB_NAMESPACE, + ATTR_DB_OPERATION_NAME, + ATTR_DB_QUERY_TEXT, + ATTR_DB_SYSTEM_NAME, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} from '@opentelemetry/semantic-conventions'; +import { + ATTR_DB_NAME, + ATTR_DB_OPERATION, + ATTR_DB_SQL_TABLE, + ATTR_DB_STATEMENT, + ATTR_DB_SYSTEM, + ATTR_DB_USER, + ATTR_NET_PEER_NAME, + ATTR_NET_PEER_PORT, + ATTR_NET_TRANSPORT, +} from './semconv'; + +const PACKAGE_NAME = '@sentry/instrumentation-knex'; + +const contextSymbol = Symbol('opentelemetry.instrumentation-knex.context'); +const DEFAULT_CONFIG: KnexInstrumentationConfig = { + maxQueryLength: 1022, + requireParentSpan: false, +}; + +export class KnexInstrumentation extends InstrumentationBase { + private _semconvStability: SemconvStability; + + constructor(config: KnexInstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); + + this._semconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); + } + + override setConfig(config: KnexInstrumentationConfig = {}) { + super.setConfig({ ...DEFAULT_CONFIG, ...config }); + } + + init() { + const module = new InstrumentationNodeModuleDefinition(constants.MODULE_NAME, constants.SUPPORTED_VERSIONS); + + module.files.push( + this.getClientNodeModuleFileInstrumentation('src'), + this.getClientNodeModuleFileInstrumentation('lib'), + this.getRunnerNodeModuleFileInstrumentation('src'), + this.getRunnerNodeModuleFileInstrumentation('lib'), + this.getRunnerNodeModuleFileInstrumentation('lib/execution'), + ); + + return module; + } + + private getRunnerNodeModuleFileInstrumentation(basePath: string) { + return new InstrumentationNodeModuleFile( + `knex/${basePath}/runner.js`, + constants.SUPPORTED_VERSIONS, + (Runner: any, moduleVersion?: string) => { + this.ensureWrapped(Runner.prototype, 'query', this.createQueryWrapper(moduleVersion)); + return Runner; + }, + (Runner: any, _moduleVersion?: string) => { + this._unwrap(Runner.prototype, 'query'); + return Runner; + }, + ); + } + + private getClientNodeModuleFileInstrumentation(basePath: string) { + return new InstrumentationNodeModuleFile( + `knex/${basePath}/client.js`, + constants.SUPPORTED_VERSIONS, + (Client: any) => { + this.ensureWrapped(Client.prototype, 'queryBuilder', this.storeContext.bind(this)); + this.ensureWrapped(Client.prototype, 'schemaBuilder', this.storeContext.bind(this)); + this.ensureWrapped(Client.prototype, 'raw', this.storeContext.bind(this)); + return Client; + }, + (Client: any) => { + this._unwrap(Client.prototype, 'queryBuilder'); + this._unwrap(Client.prototype, 'schemaBuilder'); + this._unwrap(Client.prototype, 'raw'); + return Client; + }, + ); + } + + private createQueryWrapper(moduleVersion?: string) { + const instrumentation = this; + + return function wrapQuery(original: (...args: any[]) => any) { + return function wrapped_logging_method(this: any, query: any) { + const config = this.client.config; + + const table = utils.extractTableName(this.builder); + const operation = query?.method; + const connectionString = config?.connection?.connectionString; + const name = + config?.connection?.filename || + config?.connection?.database || + utils.extractDatabaseFromConnectionString(connectionString); + const { maxQueryLength } = instrumentation.getConfig(); + + const attributes: api.Attributes = { + 'knex.version': moduleVersion, + }; + const transport = config?.connection?.filename === ':memory:' ? 'inproc' : undefined; + + if (instrumentation._semconvStability & SemconvStability.OLD) { + Object.assign(attributes, { + [ATTR_DB_SYSTEM]: utils.mapSystem(this.client.driverName), + [ATTR_DB_SQL_TABLE]: table, + [ATTR_DB_OPERATION]: operation, + [ATTR_DB_USER]: config?.connection?.user, + [ATTR_DB_NAME]: name, + [ATTR_NET_PEER_NAME]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString), + [ATTR_NET_PEER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString), + [ATTR_NET_TRANSPORT]: transport, + }); + } + if (instrumentation._semconvStability & SemconvStability.STABLE) { + Object.assign(attributes, { + [ATTR_DB_SYSTEM_NAME]: utils.mapSystem(this.client.driverName), + [ATTR_DB_COLLECTION_NAME]: table, + [ATTR_DB_OPERATION_NAME]: operation, + [ATTR_DB_NAMESPACE]: name, + [ATTR_SERVER_ADDRESS]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString), + [ATTR_SERVER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString), + }); + } + if (maxQueryLength) { + const queryText = utils.limitLength(query?.sql, maxQueryLength); + if (instrumentation._semconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = queryText; + } + if (instrumentation._semconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = queryText; + } + } + + const parentContext = this.builder[contextSymbol] || api.context.active(); + const parentSpan = api.trace.getSpan(parentContext); + const hasActiveParent = parentSpan && api.trace.isSpanContextValid(parentSpan.spanContext()); + if (instrumentation._config.requireParentSpan && !hasActiveParent) { + return original.bind(this)(...arguments); + } + + const span = instrumentation.tracer.startSpan( + utils.getName(name, operation, table), + { + kind: api.SpanKind.CLIENT, + attributes, + }, + parentContext, + ); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context + .with(spanContext, original, this, ...arguments) + .then((result: unknown) => { + span.end(); + return result; + }) + .catch((err: any) => { + const formatter = utils.getFormatter(this); + const fullQuery = formatter(query.sql, query.bindings || []); + const message = err.message.replace(fullQuery + ' - ', ''); + const exc = utils.otelExceptionFromKnexError(err, message); + span.recordException(exc); + span.setStatus({ code: api.SpanStatusCode.ERROR, message }); + span.end(); + throw err; + }); + }; + }; + } + + private storeContext(original: Function) { + return function wrapped_logging_method(this: any) { + const builder = original.apply(this, arguments); + Object.defineProperty(builder, contextSymbol, { + value: api.context.active(), + }); + return builder; + }; + } + + ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any) { + if (isWrapped(obj[methodName])) { + this._unwrap(obj, methodName); + } + this._wrap(obj, methodName, wrapper); + } +} diff --git a/packages/node/src/integrations/tracing/knex/vendored/semconv.ts b/packages/node/src/integrations/tracing/knex/vendored/semconv.ts new file mode 100644 index 000000000000..c5a0f0369efb --- /dev/null +++ b/packages/node/src/integrations/tracing/knex/vendored/semconv.ts @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex + * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 + */ +/* eslint-disable */ + +/** + * @deprecated Replaced by `db.namespace`. + */ +export const ATTR_DB_NAME = 'db.name' as const; + +/** + * @deprecated Replaced by `db.operation.name`. + */ +export const ATTR_DB_OPERATION = 'db.operation' as const; + +/** + * @deprecated Replaced by `db.collection.name`. + */ +export const ATTR_DB_SQL_TABLE = 'db.sql.table' as const; + +/** + * @deprecated Replaced by `db.query.text`. + */ +export const ATTR_DB_STATEMENT = 'db.statement' as const; + +/** + * @deprecated Replaced by `db.system.name`. + */ +export const ATTR_DB_SYSTEM = 'db.system' as const; + +/** + * @deprecated Removed, no replacement at this time. + */ +export const ATTR_DB_USER = 'db.user' as const; + +/** + * @deprecated Replaced by `server.address` on client spans and `client.address` on server spans. + */ +export const ATTR_NET_PEER_NAME = 'net.peer.name' as const; + +/** + * @deprecated Replaced by `server.port` on client spans and `client.port` on server spans. + */ +export const ATTR_NET_PEER_PORT = 'net.peer.port' as const; + +/** + * @deprecated Replaced by `network.transport`. + */ +export const ATTR_NET_TRANSPORT = 'net.transport' as const; + +export const DB_SYSTEM_NAME_VALUE_SQLITE = 'sqlite' as const; diff --git a/packages/node/src/integrations/tracing/knex/vendored/types.ts b/packages/node/src/integrations/tracing/knex/vendored/types.ts new file mode 100644 index 000000000000..eff2ca5f2d1b --- /dev/null +++ b/packages/node/src/integrations/tracing/knex/vendored/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex + * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 + */ +/* eslint-disable */ + +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface KnexInstrumentationConfig extends InstrumentationConfig { + /** max query length in db.statement attribute ".." is added to the end when query is truncated */ + maxQueryLength?: number; + /** only create spans if part of an existing trace */ + requireParentSpan?: boolean; +} diff --git a/packages/node/src/integrations/tracing/knex/vendored/utils.ts b/packages/node/src/integrations/tracing/knex/vendored/utils.ts new file mode 100644 index 000000000000..da55f118fd66 --- /dev/null +++ b/packages/node/src/integrations/tracing/knex/vendored/utils.ts @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex + * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 + */ +/* eslint-disable */ + +import { Exception } from '@opentelemetry/api'; +import { DB_SYSTEM_NAME_VALUE_POSTGRESQL } from '@opentelemetry/semantic-conventions'; +import { DB_SYSTEM_NAME_VALUE_SQLITE } from './semconv'; + +type KnexError = Error & { + code?: string; +}; + +export const getFormatter = (runner: any) => { + if (runner) { + if (runner.client) { + if (runner.client._formatQuery) { + return runner.client._formatQuery.bind(runner.client); + } else if (runner.client.SqlString) { + return runner.client.SqlString.format.bind(runner.client.SqlString); + } + } + if (runner.builder) { + return runner.builder.toString.bind(runner.builder); + } + } + return () => ''; +}; + +export function otelExceptionFromKnexError(err: KnexError, message: string): Exception { + if (!(err && err instanceof Error)) { + return err; + } + + return { + message, + code: err.code, + stack: err.stack, + name: err.name, + }; +} + +const systemMap = new Map([ + ['sqlite3', DB_SYSTEM_NAME_VALUE_SQLITE], + ['pg', DB_SYSTEM_NAME_VALUE_POSTGRESQL], +]); + +export const mapSystem = (knexSystem: string) => { + return systemMap.get(knexSystem) || knexSystem; +}; + +export const getName = (db: string, operation?: string, table?: string) => { + if (operation) { + if (table) { + return `${operation} ${db}.${table}`; + } + return `${operation} ${db}`; + } + return db; +}; + +export const limitLength = (str: string, maxLength: number) => { + if (typeof str === 'string' && typeof maxLength === 'number' && 0 < maxLength && maxLength < str.length) { + return str.substring(0, maxLength) + '..'; + } + return str; +}; + +export const extractDatabaseFromConnectionString = (connectionString?: string): string | undefined => { + if (!connectionString) return undefined; + try { + const db = new URL(connectionString).pathname?.replace(/^\//, ''); + return db || undefined; + } catch { + return undefined; + } +}; + +export const extractHostFromConnectionString = (connectionString?: string): string | undefined => { + if (!connectionString) return undefined; + try { + return new URL(connectionString).hostname || undefined; + } catch { + return undefined; + } +}; + +export const extractPortFromConnectionString = (connectionString?: string): number | undefined => { + if (!connectionString) return undefined; + try { + const port = new URL(connectionString).port; + return port ? parseInt(port, 10) : undefined; + } catch { + return undefined; + } +}; + +export const extractTableName = (builder: any): string => { + const table = builder?._single?.table; + if (typeof table === 'object') { + return extractTableName(table); + } + return table; +}; diff --git a/yarn.lock b/yarn.lock index ce6ab9046d5d..ec9aab2ad21b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6258,14 +6258,6 @@ "@opentelemetry/instrumentation" "^0.214.0" "@opentelemetry/semantic-conventions" "^1.30.0" -"@opentelemetry/instrumentation-knex@0.58.0": - version "0.58.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.58.0.tgz#48878fe40bc48834d6b4c4148433c84524a2558a" - integrity sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw== - dependencies: - "@opentelemetry/instrumentation" "^0.214.0" - "@opentelemetry/semantic-conventions" "^1.33.1" - "@opentelemetry/instrumentation-koa@0.62.0": version "0.62.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz#65fdf96c1b1ffb382167cd3b7a244631afd0cc1f" @@ -6422,7 +6414,7 @@ "@opentelemetry/resources" "2.6.1" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0": +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0": version "1.40.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3" integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw== @@ -28441,7 +28433,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2"