From c8f6e1a14386efa057f02890bbeafa9bbc749525 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 17 Jun 2026 14:05:57 -0400 Subject: [PATCH] ref(node): Streamline mongodb instrumentation Streamlines the vendored `mongodb` instrumentation to use Sentry's span APIs instead of the OpenTelemetry tracing APIs, following the redis/pg/mongoose precedent. - Replace `tracer.startSpan` with `startInactiveSpan`, the `trace.getSpan(context.active())` + `requireParentSpan` guard with a `getActiveSpan()` check, `SpanStatusCode.ERROR` with `SPAN_STATUS_ERROR`, and the `context.with(...)`/`context.bind(...)` propagation with `withActiveSpan` (result-handler re-activation and the pool `checkOut` rebind). Drop `recordException` and the `_diag` logger. - Drop the OTel connection-usage metrics: the SDK wires up no `MeterProvider`, so `this.meter` is the no-op meter and every `add` was dead. Also removes the session and connect patches that existed only to feed those metrics (they create no spans, so span output is unchanged). - Drop the `SemconvStability` dual-emission and keep the OLD semconv attributes only (the STABLE path was env-gated behind `OTEL_SEMCONV_STABILITY_OPT_IN` and never enabled by the SDK). - Remove config the SDK never passes (`responseHook`, the configurable `dbStatementSerializer`, `requireParentSpan`); move the always-on statement serializer into the vendored code and bake the `auto.db.otel.mongo` origin into the span attributes instead of an `index.ts` responseHook. - Drop the blanket eslint-disable, type the module, and split the helpers (`utils.ts`) and wrap factories (`patches.ts`) out of the class to keep each vendored file within `max-lines`. Removes the config-passing `Mongo` unit test (its serializer logic is covered end-to-end by the integration suite) and extends the real suite with an error path: a server-rejected query asserts `status: 'internal_error'` with the origin and `db.statement` preserved. --- .../suites/tracing/mongodb/scenario.mjs | 6 + .../suites/tracing/mongodb/test.ts | 19 +- .../src/integrations/tracing/mongo/index.ts | 58 +- .../tracing/mongo/vendored/instrumentation.ts | 802 +----------------- .../tracing/mongo/vendored/internal-types.ts | 79 +- .../tracing/mongo/vendored/patches.ts | 240 ++++++ .../tracing/mongo/vendored/semconv.ts | 57 -- .../tracing/mongo/vendored/types.ts | 70 -- .../tracing/mongo/vendored/utils.ts | 240 ++++++ .../test/integrations/tracing/mongo.test.ts | 72 -- 10 files changed, 534 insertions(+), 1109 deletions(-) create mode 100644 packages/node/src/integrations/tracing/mongo/vendored/patches.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/vendored/types.ts create mode 100644 packages/node/src/integrations/tracing/mongo/vendored/utils.ts delete mode 100644 packages/node/test/integrations/tracing/mongo.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.mjs index a781e8fb63c3..8a4377e98301 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/scenario.mjs @@ -28,6 +28,12 @@ async function run() { await collection.findOne({ title: 'South Park' }); await collection.find({ title: 'South Park' }).toArray(); + + // Issue a query the server rejects to exercise the error-status span path. + await collection + .find({ $thisOperatorDoesNotExist: 1 }) + .toArray() + .catch(() => {}); } finally { await client.close(); } diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index bbd38e4aed28..68a32df6181b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -97,6 +97,22 @@ describe('MongoDB auto-instrumentation', () => { origin: 'auto.db.otel.mongo', }); + // A query the server rejects: same attributes as a successful find, but with an error status. + const SPAN_FIND_ERROR_MATCHER = expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.mongo', + 'sentry.op': 'db', + 'db.system': 'mongodb', + 'db.operation': 'find', + 'db.statement': '{"$thisOperatorDoesNotExist":"?"}', + 'otel.kind': 'CLIENT', + }), + description: '{"$thisOperatorDoesNotExist":"?"}', + op: 'db', + origin: 'auto.db.otel.mongo', + status: 'internal_error', + }); + const SPAN_ENDSESSIONS_MATCHER = expect.objectContaining({ data: { 'sentry.origin': 'auto.db.otel.mongo', @@ -147,7 +163,7 @@ describe('MongoDB auto-instrumentation', () => { }, {}); expect(operationCounts).toEqual({ - find: 3, + find: 4, isMaster: 2, insert: 1, update: 1, @@ -158,6 +174,7 @@ describe('MongoDB auto-instrumentation', () => { expect(spans).toContainEqual(SPAN_INSERT_MATCHER); expect(spans).toContainEqual(SPAN_ISMASTER_MATCHER); expect(spans).toContainEqual(SPAN_UPDATE_MATCHER); + expect(spans).toContainEqual(SPAN_FIND_ERROR_MATCHER); expect(spans).toContainEqual(SPAN_ENDSESSIONS_MATCHER); }, }) diff --git a/packages/node/src/integrations/tracing/mongo/index.ts b/packages/node/src/integrations/tracing/mongo/index.ts index 270390d87cd9..c1de44f0161d 100644 --- a/packages/node/src/integrations/tracing/mongo/index.ts +++ b/packages/node/src/integrations/tracing/mongo/index.ts @@ -1,65 +1,11 @@ import { MongoDBInstrumentation } from './vendored/instrumentation'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mongo'; -export const instrumentMongo = generateInstrumentOnce( - INTEGRATION_NAME, - () => - new MongoDBInstrumentation({ - dbStatementSerializer: _defaultDbStatementSerializer, - responseHook(span) { - addOriginToSpan(span, 'auto.db.otel.mongo'); - }, - }), -); - -/** - * Replaces values in document with '?', hiding PII and helping grouping. - */ -export function _defaultDbStatementSerializer(commandObj: Record): string { - const resultObj = _scrubStatement(commandObj); - return JSON.stringify(resultObj); -} - -function _scrubStatement(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map(element => _scrubStatement(element)); - } - - if (isCommandObj(value)) { - const initial: Record = {}; - return Object.entries(value) - .map(([key, element]) => [key, _scrubStatement(element)]) - .reduce((prev, current) => { - if (isCommandEntry(current)) { - prev[current[0]] = current[1]; - } - return prev; - }, initial); - } - - // A value like string or number, possible contains PII, scrub it - return '?'; -} - -function isCommandObj(value: Record | unknown): value is Record { - return typeof value === 'object' && value !== null && !isBuffer(value); -} - -function isBuffer(value: unknown): boolean { - let isBuffer = false; - if (typeof Buffer !== 'undefined') { - isBuffer = Buffer.isBuffer(value); - } - return isBuffer; -} - -function isCommandEntry(value: [string, unknown] | unknown): value is [string, unknown] { - return Array.isArray(value); -} +export const instrumentMongo = generateInstrumentOnce(INTEGRATION_NAME, () => new MongoDBInstrumentation()); const _mongoIntegration = (() => { return { diff --git a/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts index 11986a0d6c68..292ff2222425 100644 --- a/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts +++ b/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts @@ -5,109 +5,33 @@ * NOTICE from the Sentry authors: * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs + * - Dropped the OTel connection-usage metrics (no Sentry MeterProvider consumes them) and the + * session/connect patches that existed only to feed them + * - Dropped the env-gated stable-semconv dual emission; only the (default) old semantic + * conventions are emitted, matching the previous default span output */ -/* eslint-disable */ -import { context, trace, Span, SpanKind, SpanStatusCode, UpDownCounter, type Attributes } from '@opentelemetry/api'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - isWrapped, - safeExecuteInTheMiddle, - SemconvStability, - semconvStabilityFromStr, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -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_CONNECTION_STRING, - ATTR_DB_MONGODB_COLLECTION, - ATTR_DB_NAME, - ATTR_DB_OPERATION, - ATTR_DB_STATEMENT, - ATTR_DB_SYSTEM, - ATTR_NET_PEER_NAME, - ATTR_NET_PEER_PORT, - DB_SYSTEM_NAME_VALUE_MONGODB, - DB_SYSTEM_VALUE_MONGODB, - METRIC_DB_CLIENT_CONNECTIONS_USAGE, -} from './semconv'; -import { MongoDBInstrumentationConfig, CommandResult } from './types'; -import { - CursorState, - ServerSession, - MongodbCommandType, - MongoInternalCommand, - MongodbNamespace, - MongoInternalTopology, - WireProtocolInternal, - V4Connection, - V4ConnectionPool, - Replacer, -} from './internal-types'; -import { V4Connect, V4Session } from './internal-types'; +import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { SDK_VERSION } from '@sentry/core'; +import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; +import type { WireProtocolInternal } from './internal-types'; +import * as patches from './patches'; const PACKAGE_NAME = '@sentry/instrumentation-mongodb'; -const DEFAULT_CONFIG: MongoDBInstrumentationConfig = { - requireParentSpan: true, -}; - -/** mongodb instrumentation plugin for OpenTelemetry */ -export class MongoDBInstrumentation extends InstrumentationBase { - private _netSemconvStability!: SemconvStability; - private _dbSemconvStability!: SemconvStability; - declare private _connectionsUsage: UpDownCounter; - declare private _poolName: string; - - constructor(config: MongoDBInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); - this._setSemconvStabilityFromEnv(); - } - - // Used for testing. - private _setSemconvStabilityFromEnv() { - this._netSemconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); - this._dbSemconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); - } - - override setConfig(config: MongoDBInstrumentationConfig = {}) { - super.setConfig({ ...DEFAULT_CONFIG, ...config }); - } - - override _updateMetricInstruments() { - this._connectionsUsage = this.meter.createUpDownCounter(METRIC_DB_CLIENT_CONNECTIONS_USAGE, { - description: 'The number of connections that are currently in state described by the state attribute.', - unit: '{connection}', - }); +/** mongodb instrumentation plugin */ +export class MongoDBInstrumentation extends InstrumentationBase { + public constructor(config: InstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, config); } - /** - * Convenience function for updating the `db.client.connections.usage` metric. - * The name "count" comes from the eventual replacement for this metric per - * https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/#database-client-connection-count - */ - private _connCountAdd(n: number, poolName: string, state: string) { - this._connectionsUsage?.add(n, { 'pool.name': poolName, state }); - } - - init() { - const { v3PatchConnection: v3PatchConnection, v3UnpatchConnection: v3UnpatchConnection } = - this._getV3ConnectionPatches(); + public init(): InstrumentationNodeModuleDefinition[] { + const { v3PatchConnection, v3UnpatchConnection } = this._getV3ConnectionPatches(); - const { v4PatchConnect, v4UnpatchConnect } = this._getV4ConnectPatches(); const { v4PatchConnectionCallback, v4PatchConnectionPromise, v4UnpatchConnection } = this._getV4ConnectionPatches(); const { v4PatchConnectionPool, v4UnpatchConnectionPool } = this._getV4ConnectionPoolPatches(); - const { v4PatchSessions, v4UnpatchSessions } = this._getV4SessionsPatches(); return [ new InstrumentationNodeModuleDefinition('mongodb', ['>=3.3.0 <4'], undefined, undefined, [ @@ -137,18 +61,6 @@ export class MongoDBInstrumentation extends InstrumentationBase=4.0.0 <8'], - v4PatchConnect, - v4UnpatchConnect, - ), - new InstrumentationNodeModuleFile( - 'mongodb/lib/sessions.js', - ['>=4.0.0 <8'], - v4PatchSessions, - v4UnpatchSessions, - ), ]), ]; } @@ -160,32 +72,32 @@ export class MongoDBInstrumentation extends InstrumentationBase { @@ -200,67 +112,7 @@ export class MongoDBInstrumentation extends InstrumentationBase() { - return { - v4PatchSessions: (moduleExports: any) => { - if (isWrapped(moduleExports.acquire)) { - this._unwrap(moduleExports, 'acquire'); - } - this._wrap(moduleExports.ServerSessionPool.prototype, 'acquire', this._getV4AcquireCommand()); - - if (isWrapped(moduleExports.release)) { - this._unwrap(moduleExports, 'release'); - } - this._wrap(moduleExports.ServerSessionPool.prototype, 'release', this._getV4ReleaseCommand()); - return moduleExports; - }, - v4UnpatchSessions: (moduleExports?: T) => { - if (moduleExports === undefined) return; - if (isWrapped(moduleExports.acquire)) { - this._unwrap(moduleExports, 'acquire'); - } - if (isWrapped(moduleExports.release)) { - this._unwrap(moduleExports, 'release'); - } - }, - }; - } - - private _getV4AcquireCommand() { - const instrumentation = this; - return (original: V4Session['acquire']) => { - return function patchAcquire(this: any) { - const nSessionsBeforeAcquire = this.sessions.length; - const session = original.call(this); - const nSessionsAfterAcquire = this.sessions.length; - - if (nSessionsBeforeAcquire === nSessionsAfterAcquire) { - //no session in the pool. a new session was created and used - instrumentation._connCountAdd(1, instrumentation._poolName, 'used'); - } else if (nSessionsBeforeAcquire - 1 === nSessionsAfterAcquire) { - //a session was already in the pool. remove it from the pool and use it. - instrumentation._connCountAdd(-1, instrumentation._poolName, 'idle'); - instrumentation._connCountAdd(1, instrumentation._poolName, 'used'); - } - return session; - }; - }; - } - - private _getV4ReleaseCommand() { - const instrumentation = this; - return (original: V4Session['release']) => { - return function patchRelease(this: any, session: ServerSession) { - const cmdPromise = original.call(this, session); - - instrumentation._connCountAdd(-1, instrumentation._poolName, 'used'); - instrumentation._connCountAdd(1, instrumentation._poolName, 'idle'); - return cmdPromise; - }; - }; - } - - private _getV4ConnectionPoolPatches() { + private _getV4ConnectionPoolPatches() { return { v4PatchConnectionPool: (moduleExports: any) => { const poolPrototype = moduleExports.ConnectionPool.prototype; @@ -269,7 +121,7 @@ export class MongoDBInstrumentation extends InstrumentationBase { @@ -280,88 +132,22 @@ export class MongoDBInstrumentation extends InstrumentationBase() { - return { - v4PatchConnect: (moduleExports: any) => { - if (isWrapped(moduleExports.connect)) { - this._unwrap(moduleExports, 'connect'); - } - - this._wrap(moduleExports, 'connect', this._getV4ConnectCommand()); - return moduleExports; - }, - v4UnpatchConnect: (moduleExports?: T) => { - if (moduleExports === undefined) return; - - this._unwrap(moduleExports, 'connect'); - }, - }; - } - - // This patch will become unnecessary once - // https://jira.mongodb.org/browse/NODE-5639 is done. - private _getV4ConnectionPoolCheckOut() { - return (original: V4ConnectionPool['checkOut']) => { - return function patchedCheckout(this: unknown, callback: any) { - const patchedCallback = context.bind(context.active(), callback); - return original.call(this, patchedCallback); - }; - }; - } - - private _getV4ConnectCommand() { - const instrumentation = this; - - return (original: V4Connect['connectCallback'] | V4Connect['connectPromise']) => { - return function patchedConnect(this: unknown, options: any, callback: any) { - // from v6.4 `connect` method only accepts an options param and returns a promise - // with the connection - if (original.length === 1) { - const result = (original as V4Connect['connectPromise']).call(this, options); - if (result && typeof result.then === 'function') { - result.then( - () => instrumentation.setPoolName(options), - // this handler is set to pass the lint rules - () => undefined, - ); - } - return result; - } - - // Earlier versions expects a callback param and return void - const patchedCallback = function (err: any, conn: any) { - if (err || !conn) { - callback(err, conn); - return; - } - instrumentation.setPoolName(options); - callback(err, conn); - }; - - return (original as V4Connect['connectCallback']).call(this, options, patchedCallback); - }; - }; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private _getV4ConnectionPatches() { + private _getV4ConnectionPatches() { return { v4PatchConnectionCallback: (moduleExports: any) => { - // patch insert operation if (isWrapped(moduleExports.Connection.prototype.command)) { this._unwrap(moduleExports.Connection.prototype, 'command'); } - this._wrap(moduleExports.Connection.prototype, 'command', this._getV4PatchCommandCallback()); + this._wrap(moduleExports.Connection.prototype, 'command', patches.getV4PatchCommandCallback()); return moduleExports; }, v4PatchConnectionPromise: (moduleExports: any) => { - // patch insert operation if (isWrapped(moduleExports.Connection.prototype.command)) { this._unwrap(moduleExports.Connection.prototype, 'command'); } - this._wrap(moduleExports.Connection.prototype, 'command', this._getV4PatchCommandPromise()); + this._wrap(moduleExports.Connection.prototype, 'command', patches.getV4PatchCommandPromise()); return moduleExports; }, v4UnpatchConnection: (moduleExports?: any) => { @@ -370,538 +156,4 @@ export class MongoDBInstrumentation extends InstrumentationBase { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function, - ) { - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - - const resultHandler = typeof options === 'function' ? options : callback; - if (skipInstrumentation || typeof resultHandler !== 'function' || typeof ops !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, ops, options); - } else { - return original.call(this, server, ns, ops, options, callback); - } - } - - const attributes = instrumentation._getV3SpanAttributes( - ns, - server, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ops[0] as any, - operationName, - ); - const spanName = instrumentation._spanNameFromAttrs(attributes); - const span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - - const patchedCallback = instrumentation._patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, ops, patchedCallback); - } else { - return original.call(this, server, ns, ops, options, patchedCallback); - } - }; - }; - } - - /** Creates spans for command operation */ - private _getV3PatchCommand() { - const instrumentation = this; - return (original: WireProtocolInternal['command']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - options: unknown | Function, - callback?: Function, - ) { - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - - const resultHandler = typeof options === 'function' ? options : callback; - - if (skipInstrumentation || typeof resultHandler !== 'function' || typeof cmd !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, options); - } else { - return original.call(this, server, ns, cmd, options, callback); - } - } - - const commandType = MongoDBInstrumentation._getCommandType(cmd); - const operationName = commandType === MongodbCommandType.UNKNOWN ? undefined : commandType; - const attributes = instrumentation._getV3SpanAttributes(ns, server, cmd, operationName); - const spanName = instrumentation._spanNameFromAttrs(attributes); - const span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - - const patchedCallback = instrumentation._patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, patchedCallback); - } else { - return original.call(this, server, ns, cmd, options, patchedCallback); - } - }; - }; - } - - /** Creates spans for command operation */ - private _getV4PatchCommandCallback() { - const instrumentation = this; - return (original: V4Connection['commandCallback']) => { - return function patchedV4ServerCommand( - this: any, - ns: MongodbNamespace, - cmd: any, - options: undefined | unknown, - callback: any, - ) { - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - const resultHandler = callback; - const commandType = Object.keys(cmd)[0]; - - if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return original.call(this, ns, cmd, options, callback); - } - - let span = undefined; - if (!skipInstrumentation) { - const attributes = instrumentation._getV4SpanAttributes(this, ns, cmd, commandType); - const spanName = instrumentation._spanNameFromAttrs(attributes); - span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - } - const patchedCallback = instrumentation._patchEnd(span, resultHandler, this.id, commandType); - - return original.call(this, ns, cmd, options, patchedCallback); - }; - }; - } - - private _getV4PatchCommandPromise() { - const instrumentation = this; - return (original: V4Connection['commandPromise']) => { - return function patchedV4ServerCommand(this: any, ...args: Parameters) { - const [ns, cmd] = args; - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - - const commandType = Object.keys(cmd)[0]; - const resultHandler = () => undefined; - - if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return original.apply(this, args); - } - - let span = undefined; - if (!skipInstrumentation) { - const attributes = instrumentation._getV4SpanAttributes(this, ns, cmd, commandType); - const spanName = instrumentation._spanNameFromAttrs(attributes); - span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - } - - const patchedCallback = instrumentation._patchEnd(span, resultHandler, this.id, commandType); - - const result = original.apply(this, args); - result.then( - (res: any) => patchedCallback(null, res), - (err: any) => patchedCallback(err), - ); - - return result; - }; - }; - } - - /** Creates spans for find operation */ - private _getV3PatchFind() { - const instrumentation = this; - return (original: WireProtocolInternal['query']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - cursorState: CursorState, - options: unknown | Function, - callback?: Function, - ) { - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - const resultHandler = typeof options === 'function' ? options : callback; - - if (skipInstrumentation || typeof resultHandler !== 'function' || typeof cmd !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, cursorState, options); - } else { - return original.call(this, server, ns, cmd, cursorState, options, callback); - } - } - - const attributes = instrumentation._getV3SpanAttributes(ns, server, cmd, 'find'); - const spanName = instrumentation._spanNameFromAttrs(attributes); - const span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - - const patchedCallback = instrumentation._patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, cursorState, patchedCallback); - } else { - return original.call(this, server, ns, cmd, cursorState, options, patchedCallback); - } - }; - }; - } - - /** Creates spans for find operation */ - private _getV3PatchCursor() { - const instrumentation = this; - return (original: WireProtocolInternal['getMore']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cursorState: CursorState, - batchSize: number, - options: unknown | Function, - callback?: Function, - ) { - const currentSpan = trace.getSpan(context.active()); - const skipInstrumentation = instrumentation._checkSkipInstrumentation(currentSpan); - - const resultHandler = typeof options === 'function' ? options : callback; - - if (skipInstrumentation || typeof resultHandler !== 'function') { - if (typeof options === 'function') { - return original.call(this, server, ns, cursorState, batchSize, options); - } else { - return original.call(this, server, ns, cursorState, batchSize, options, callback); - } - } - - const attributes = instrumentation._getV3SpanAttributes(ns, server, cursorState.cmd, 'getMore'); - const spanName = instrumentation._spanNameFromAttrs(attributes); - const span = instrumentation.tracer.startSpan(spanName, { - kind: SpanKind.CLIENT, - attributes, - }); - - const patchedCallback = instrumentation._patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cursorState, batchSize, patchedCallback); - } else { - return original.call(this, server, ns, cursorState, batchSize, options, patchedCallback); - } - }; - }; - } - - /** - * Get the mongodb command type from the object. - * @param command Internal mongodb command object - */ - private static _getCommandType(command: MongoInternalCommand): MongodbCommandType { - if (command.createIndexes !== undefined) { - return MongodbCommandType.CREATE_INDEXES; - } else if (command.findandmodify !== undefined) { - return MongodbCommandType.FIND_AND_MODIFY; - } else if (command.ismaster !== undefined) { - return MongodbCommandType.IS_MASTER; - } else if (command.count !== undefined) { - return MongodbCommandType.COUNT; - } else if (command.aggregate !== undefined) { - return MongodbCommandType.AGGREGATE; - } else { - return MongodbCommandType.UNKNOWN; - } - } - - /** - * Determine a span's attributes by fetching related metadata from the context - * @param connectionCtx mongodb internal connection context - * @param ns mongodb namespace - * @param command mongodb internal representation of a command - */ - private _getV4SpanAttributes( - connectionCtx: any, - ns: MongodbNamespace, - command?: any, - operation?: string, - ): Attributes { - let host, port: undefined | string; - if (connectionCtx) { - const hostParts = typeof connectionCtx.address === 'string' ? connectionCtx.address.split(':') : ''; - if (hostParts.length === 2) { - host = hostParts[0]; - port = hostParts[1]; - } - } - // capture parameters within the query as well if enhancedDatabaseReporting is enabled. - let commandObj: Record; - if (command?.documents && command.documents[0]) { - commandObj = command.documents[0]; - } else if (command?.cursors) { - commandObj = command.cursors; - } else { - commandObj = command; - } - - return this._getSpanAttributes(ns.db, ns.collection, host, port, commandObj, operation); - } - - /** - * Determine a span's attributes by fetching related metadata from the context - * @param ns mongodb namespace - * @param topology mongodb internal representation of the network topology - * @param command mongodb internal representation of a command - */ - private _getV3SpanAttributes( - ns: string, - topology: MongoInternalTopology, - command?: MongoInternalCommand, - operation?: string | undefined, - ): Attributes { - // Extract host/port info. - let host: undefined | string; - let port: undefined | string; - if (topology && topology.s) { - host = topology.s.options?.host ?? topology.s.host; - port = (topology.s.options?.port ?? topology.s.port)?.toString(); - if (host == null || port == null) { - const address = topology.description?.address; - if (address) { - const addressSegments = address.split(':'); - host = addressSegments[0]; - port = addressSegments[1]; - } - } - } - - // The namespace is a combination of the database name and the name of the - // collection or index, like so: [database-name].[collection-or-index-name]. - // It could be a string or an instance of MongoDBNamespace, as such we - // always coerce to a string to extract db and collection. - const [dbName, dbCollection] = ns.toString().split('.'); - // capture parameters within the query as well if enhancedDatabaseReporting is enabled. - const commandObj = command?.query ?? command?.q ?? command; - - return this._getSpanAttributes(dbName, dbCollection, host, port, commandObj, operation); - } - - private _getSpanAttributes( - dbName?: string, - dbCollection?: string, - host?: undefined | string, - port?: undefined | string, - commandObj?: any, - operation?: string | undefined, - ): Attributes { - const attributes: Attributes = {}; - - if (this._dbSemconvStability & SemconvStability.OLD) { - attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_MONGODB; - attributes[ATTR_DB_NAME] = dbName; - attributes[ATTR_DB_MONGODB_COLLECTION] = dbCollection; - attributes[ATTR_DB_OPERATION] = operation; - attributes[ATTR_DB_CONNECTION_STRING] = `mongodb://${host}:${port}/${dbName}`; - } - if (this._dbSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MONGODB; - attributes[ATTR_DB_NAMESPACE] = dbName; - attributes[ATTR_DB_OPERATION_NAME] = operation; - attributes[ATTR_DB_COLLECTION_NAME] = dbCollection; - } - - if (host && port) { - if (this._netSemconvStability & SemconvStability.OLD) { - attributes[ATTR_NET_PEER_NAME] = host; - } - if (this._netSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_SERVER_ADDRESS] = host; - } - const portNumber = parseInt(port, 10); - if (!isNaN(portNumber)) { - if (this._netSemconvStability & SemconvStability.OLD) { - attributes[ATTR_NET_PEER_PORT] = portNumber; - } - if (this._netSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_SERVER_PORT] = portNumber; - } - } - } - - if (commandObj) { - const { dbStatementSerializer: configDbStatementSerializer } = this.getConfig(); - const dbStatementSerializer = - typeof configDbStatementSerializer === 'function' - ? configDbStatementSerializer - : this._defaultDbStatementSerializer.bind(this); - - safeExecuteInTheMiddle( - () => { - const query = dbStatementSerializer(commandObj); - if (this._dbSemconvStability & SemconvStability.OLD) { - attributes[ATTR_DB_STATEMENT] = query; - } - if (this._dbSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_DB_QUERY_TEXT] = query; - } - }, - err => { - if (err) { - this._diag.error('Error running dbStatementSerializer hook', err); - } - }, - true, - ); - } - - return attributes; - } - - private _spanNameFromAttrs(attributes: Attributes): string { - let spanName; - if (this._dbSemconvStability & SemconvStability.STABLE) { - // https://opentelemetry.io/docs/specs/semconv/database/database-spans/#name - spanName = - [attributes[ATTR_DB_OPERATION_NAME], attributes[ATTR_DB_COLLECTION_NAME]].filter(attr => attr).join(' ') || - DB_SYSTEM_NAME_VALUE_MONGODB; - } else { - spanName = `mongodb.${attributes[ATTR_DB_OPERATION] || 'command'}`; - } - return spanName; - } - - private _getDefaultDbStatementReplacer(): Replacer { - const seen = new WeakSet(); - return (_key, value) => { - // undefined, boolean, number, bigint, string, symbol, function || null - if (typeof value !== 'object' || !value) return '?'; - - // objects (including arrays) - if (seen.has(value)) return '[Circular]'; - seen.add(value); - return value; - }; - } - - private _defaultDbStatementSerializer(commandObj: Record) { - const { enhancedDatabaseReporting } = this.getConfig(); - - if (enhancedDatabaseReporting) { - return JSON.stringify(commandObj); - } - - return JSON.stringify(commandObj, this._getDefaultDbStatementReplacer()); - } - - /** - * Triggers the response hook in case it is defined. - * @param span The span to add the results to. - * @param result The command result - */ - private _handleExecutionResult(span: Span, result: CommandResult) { - const { responseHook } = this.getConfig(); - if (typeof responseHook === 'function') { - safeExecuteInTheMiddle( - () => { - responseHook(span, { data: result }); - }, - err => { - if (err) { - this._diag.error('Error running response hook', err); - } - }, - true, - ); - } - } - - /** - * Ends a created span. - * @param span The created span to end. - * @param resultHandler A callback function. - * @param connectionId: The connection ID of the Command response. - */ - private _patchEnd( - span: Span | undefined, - resultHandler: Function, - connectionId?: number, - commandType?: string, - ): Function { - // mongodb is using "tick" when calling a callback, this way the context - // in final callback (resultHandler) is lost - const activeContext = context.active(); - const instrumentation = this; - let spanEnded = false; - - return function patchedEnd(this: {}, ...args: unknown[]) { - if (!spanEnded) { - spanEnded = true; - const error = args[0]; - if (span) { - if (error instanceof Error) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - } else { - const result = args[1] as CommandResult; - instrumentation._handleExecutionResult(span, result); - } - span.end(); - } - - if (commandType === 'endSessions') { - instrumentation._connCountAdd(-1, instrumentation._poolName, 'idle'); - } - } - - return context.with(activeContext, () => { - return resultHandler.apply(this, args); - }); - }; - } - private setPoolName(options: any) { - const host = options.hostAddress?.host; - const port = options.hostAddress?.port; - const database = options.dbName; - const poolName = `mongodb://${host}:${port}/${database}`; - this._poolName = poolName; - } - - private _checkSkipInstrumentation(currentSpan: Span | undefined) { - const requireParentSpan = this.getConfig().requireParentSpan; - const hasNoParentSpan = currentSpan === undefined; - return requireParentSpan === true && hasNoParentSpan; - } } diff --git a/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts b/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts index 88d4e8f536d4..3a192ffa11d6 100644 --- a/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts +++ b/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts @@ -5,47 +5,9 @@ * NOTICE from the Sentry authors: * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 + * - Trimmed to the driver-internal types actually used by the instrumentation */ -/* eslint-disable */ -import { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { Span } from '@opentelemetry/api'; - -export interface MongoDBInstrumentationExecutionResponseHook { - (span: Span, responseInfo: MongoResponseHookInformation): void; -} - -/** - * Function that can be used to serialize db.statement tag - * @param cmd - MongoDB command object - * - * @returns serialized string that will be used as the db.statement attribute. - */ -export type DbStatementSerializer = (cmd: Record) => string; - -export interface MongoDBInstrumentationConfig extends InstrumentationConfig { - /** - * If true, additional information about query parameters and - * results will be attached (as `attributes`) to spans representing - * database operations. - */ - enhancedDatabaseReporting?: boolean; - - /** - * Hook that allows adding custom span attributes based on the data - * returned from MongoDB actions. - * - * @default undefined - */ - responseHook?: MongoDBInstrumentationExecutionResponseHook; - - /** - * Custom serializer function for the db.statement tag - */ - dbStatementSerializer?: DbStatementSerializer; -} - -export type Func = (...args: unknown[]) => T; export type MongoInternalCommand = { findandmodify: boolean; createIndexes: boolean; @@ -59,26 +21,8 @@ export type MongoInternalCommand = { u?: Record; }; -export type ServerSession = { - id: any; - lastUse: number; - txnNumber: number; - isDirty: boolean; -}; - export type CursorState = { cmd: MongoInternalCommand } & Record; -export interface MongoResponseHookInformation { - data: CommandResult; -} - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/connection/command_result.js -export type CommandResult = { - result?: unknown; - connection?: unknown; - message?: unknown; -}; - // https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/wireprotocol/index.js export type WireProtocolInternal = { insert: ( @@ -191,24 +135,3 @@ export type V4ConnectionPool = { // types of callback params are not needed checkOut: (callback: (error: any, connection: any) => void) => void; }; - -export type V4Connect = { - connect: Function; - // From version 6.4.0 the method does not expect a callback and returns a promise - // https://github.com/mongodb/node-mongodb-native/blob/v6.4.0/src/cmap/connect.ts - connectPromise: (options: any) => Promise; - // Earlier versions expect a callback param and return void - // https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connect.ts - connectCallback: (options: any, callback: any) => void; -}; - -// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/sessions.ts -export type V4Session = { - acquire: () => ServerSession; - release: (session: ServerSession) => void; -}; - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer - */ -export type Replacer = (key: string, value: unknown) => unknown; diff --git a/packages/node/src/integrations/tracing/mongo/vendored/patches.ts b/packages/node/src/integrations/tracing/mongo/vendored/patches.ts new file mode 100644 index 000000000000..1d1e7ef20cc7 --- /dev/null +++ b/packages/node/src/integrations/tracing/mongo/vendored/patches.ts @@ -0,0 +1,240 @@ +/* + * 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-mongodb + * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs + */ + +import { getActiveSpan, withActiveSpan } from '@sentry/core'; +import type { + CursorState, + MongodbNamespace, + MongoInternalCommand, + MongoInternalTopology, + V4Connection, + V4ConnectionPool, + WireProtocolInternal, +} from './internal-types'; +import { MongodbCommandType } from './internal-types'; +import { + getCommandType, + getV3SpanAttributes, + getV4SpanAttributes, + patchEnd, + shouldSkipInstrumentation, + startMongoSpan, +} from './utils'; + +/** Creates spans for v3 common operations (insert/update/remove). */ +export function getV3PatchOperation(operationName: 'insert' | 'update' | 'remove') { + return (original: WireProtocolInternal[typeof operationName]) => { + return function patchedServerCommand( + this: unknown, + server: MongoInternalTopology, + ns: string, + ops: unknown[], + options: unknown | Function, + callback?: Function, + ) { + const resultHandler = typeof options === 'function' ? options : callback; + if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof ops !== 'object') { + if (typeof options === 'function') { + return original.call(this, server, ns, ops, options); + } else { + return original.call(this, server, ns, ops, options, callback); + } + } + + const span = startMongoSpan(getV3SpanAttributes(ns, server, ops[0] as any, operationName)); + + const patchedCallback = patchEnd(span, resultHandler); + // handle when options is the callback to send the correct number of args + if (typeof options === 'function') { + return original.call(this, server, ns, ops, patchedCallback); + } else { + return original.call(this, server, ns, ops, options, patchedCallback); + } + }; + }; +} + +/** Creates spans for the v3 command operation. */ +export function getV3PatchCommand() { + return (original: WireProtocolInternal['command']) => { + return function patchedServerCommand( + this: unknown, + server: MongoInternalTopology, + ns: string, + cmd: MongoInternalCommand, + options: unknown | Function, + callback?: Function, + ) { + const resultHandler = typeof options === 'function' ? options : callback; + + if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof cmd !== 'object') { + if (typeof options === 'function') { + return original.call(this, server, ns, cmd, options); + } else { + return original.call(this, server, ns, cmd, options, callback); + } + } + + const commandType = getCommandType(cmd); + const operationName = commandType === MongodbCommandType.UNKNOWN ? undefined : commandType; + const span = startMongoSpan(getV3SpanAttributes(ns, server, cmd, operationName)); + + const patchedCallback = patchEnd(span, resultHandler); + // handle when options is the callback to send the correct number of args + if (typeof options === 'function') { + return original.call(this, server, ns, cmd, patchedCallback); + } else { + return original.call(this, server, ns, cmd, options, patchedCallback); + } + }; + }; +} + +/** Creates spans for the v4 (<6.4) callback-style command operation. */ +export function getV4PatchCommandCallback() { + return (original: V4Connection['commandCallback']) => { + return function patchedV4ServerCommand( + this: any, + ns: MongodbNamespace, + cmd: any, + options: undefined | unknown, + callback: any, + ) { + const resultHandler = callback; + const commandType = Object.keys(cmd)[0]; + + if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { + return original.call(this, ns, cmd, options, callback); + } + + let span = undefined; + if (!shouldSkipInstrumentation()) { + span = startMongoSpan(getV4SpanAttributes(this, ns, cmd, commandType)); + } + const patchedCallback = patchEnd(span, resultHandler); + + return original.call(this, ns, cmd, options, patchedCallback); + }; + }; +} + +/** Creates spans for the v4 (>=6.4) promise-style command operation. */ +export function getV4PatchCommandPromise() { + return (original: V4Connection['commandPromise']) => { + return function patchedV4ServerCommand(this: any, ...args: Parameters) { + const [ns, cmd] = args; + const commandType = Object.keys(cmd)[0]; + const resultHandler = () => undefined; + + if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { + return original.apply(this, args); + } + + let span = undefined; + if (!shouldSkipInstrumentation()) { + span = startMongoSpan(getV4SpanAttributes(this, ns, cmd, commandType)); + } + + const patchedCallback = patchEnd(span, resultHandler); + + const result = original.apply(this, args); + result.then( + (res: any) => patchedCallback(null, res), + (err: any) => patchedCallback(err), + ); + + return result; + }; + }; +} + +/** Creates spans for the v3 find operation. */ +export function getV3PatchFind() { + return (original: WireProtocolInternal['query']) => { + return function patchedServerCommand( + this: unknown, + server: MongoInternalTopology, + ns: string, + cmd: MongoInternalCommand, + cursorState: CursorState, + options: unknown | Function, + callback?: Function, + ) { + const resultHandler = typeof options === 'function' ? options : callback; + + if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof cmd !== 'object') { + if (typeof options === 'function') { + return original.call(this, server, ns, cmd, cursorState, options); + } else { + return original.call(this, server, ns, cmd, cursorState, options, callback); + } + } + + const span = startMongoSpan(getV3SpanAttributes(ns, server, cmd, 'find')); + + const patchedCallback = patchEnd(span, resultHandler); + // handle when options is the callback to send the correct number of args + if (typeof options === 'function') { + return original.call(this, server, ns, cmd, cursorState, patchedCallback); + } else { + return original.call(this, server, ns, cmd, cursorState, options, patchedCallback); + } + }; + }; +} + +/** Creates spans for the v3 getMore (cursor) operation. */ +export function getV3PatchCursor() { + return (original: WireProtocolInternal['getMore']) => { + return function patchedServerCommand( + this: unknown, + server: MongoInternalTopology, + ns: string, + cursorState: CursorState, + batchSize: number, + options: unknown | Function, + callback?: Function, + ) { + const resultHandler = typeof options === 'function' ? options : callback; + + if (shouldSkipInstrumentation() || typeof resultHandler !== 'function') { + if (typeof options === 'function') { + return original.call(this, server, ns, cursorState, batchSize, options); + } else { + return original.call(this, server, ns, cursorState, batchSize, options, callback); + } + } + + const span = startMongoSpan(getV3SpanAttributes(ns, server, cursorState.cmd, 'getMore')); + + const patchedCallback = patchEnd(span, resultHandler); + // handle when options is the callback to send the correct number of args + if (typeof options === 'function') { + return original.call(this, server, ns, cursorState, batchSize, patchedCallback); + } else { + return original.call(this, server, ns, cursorState, batchSize, options, patchedCallback); + } + }; + }; +} + +// This patch will become unnecessary once https://jira.mongodb.org/browse/NODE-5639 is done. +export function getV4ConnectionPoolCheckOut() { + return (original: V4ConnectionPool['checkOut']) => { + return function patchedCheckout(this: unknown, callback: (error: any, connection: any) => void) { + // The pool runs the callback in a detached context, so re-activate the span that was + // active when `checkOut` was called — otherwise the pooled operation finds no parent. + const parentSpan = getActiveSpan(); + return original.call(this, function (this: unknown, ...args: [any, any]) { + return withActiveSpan(parentSpan ?? null, () => callback.apply(this, args)); + }); + }; + }; +} diff --git a/packages/node/src/integrations/tracing/mongo/vendored/semconv.ts b/packages/node/src/integrations/tracing/mongo/vendored/semconv.ts index 1d150a87edb3..b892caade680 100644 --- a/packages/node/src/integrations/tracing/mongo/vendored/semconv.ts +++ b/packages/node/src/integrations/tracing/mongo/vendored/semconv.ts @@ -6,7 +6,6 @@ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 */ -/* eslint-disable */ /* * This file contains a copy of unstable semantic convention definitions @@ -17,10 +16,6 @@ /** * Deprecated, use `server.address`, `server.port` attributes instead. * - * @example "Server=(localdb)\\v11.0;Integrated Security=true;" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `server.address` and `server.port`. */ export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; @@ -28,10 +23,6 @@ export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; /** * Deprecated, use `db.collection.name` instead. * - * @example "mytable" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `db.collection.name`. */ export const ATTR_DB_MONGODB_COLLECTION = 'db.mongodb.collection' as const; @@ -39,11 +30,6 @@ export const ATTR_DB_MONGODB_COLLECTION = 'db.mongodb.collection' as const; /** * Deprecated, use `db.namespace` instead. * - * @example customers - * @example main - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `db.namespace`. */ export const ATTR_DB_NAME = 'db.name' as const; @@ -51,12 +37,6 @@ export const ATTR_DB_NAME = 'db.name' as const; /** * Deprecated, use `db.operation.name` instead. * - * @example findAndModify - * @example HMSET - * @example SELECT - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `db.operation.name`. */ export const ATTR_DB_OPERATION = 'db.operation' as const; @@ -64,11 +44,6 @@ export const ATTR_DB_OPERATION = 'db.operation' as const; /** * The database statement being executed. * - * @example SELECT * FROM wuser_table - * @example SET mykey "WuValue" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `db.query.text`. */ export const ATTR_DB_STATEMENT = 'db.statement' as const; @@ -76,8 +51,6 @@ export const ATTR_DB_STATEMENT = 'db.statement' as const; /** * Deprecated, use `db.system.name` instead. * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @deprecated Replaced by `db.system.name`. */ export const ATTR_DB_SYSTEM = 'db.system' as const; @@ -85,10 +58,6 @@ export const ATTR_DB_SYSTEM = 'db.system' as const; /** * Deprecated, use `server.address` on client spans and `client.address` on server spans. * - * @example example.com - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @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; @@ -96,37 +65,11 @@ export const ATTR_NET_PEER_NAME = 'net.peer.name' as const; /** * Deprecated, use `server.port` on client spans and `client.port` on server spans. * - * @example 8080 - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * * @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; -/** - * Enum value "mongodb" for attribute {@link ATTR_DB_SYSTEM_NAME}. - * - * [MongoDB](https://www.mongodb.com/) - * - * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const DB_SYSTEM_NAME_VALUE_MONGODB = 'mongodb' as const; - /** * Enum value "mongodb" for attribute {@link ATTR_DB_SYSTEM}. - * - * MongoDB - * - * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. */ export const DB_SYSTEM_VALUE_MONGODB = 'mongodb' as const; - -/** - * Deprecated, use `db.client.connection.count` instead. - * - * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * - * @deprecated Replaced by `db.client.connection.count`. - */ -export const METRIC_DB_CLIENT_CONNECTIONS_USAGE = 'db.client.connections.usage' as const; diff --git a/packages/node/src/integrations/tracing/mongo/vendored/types.ts b/packages/node/src/integrations/tracing/mongo/vendored/types.ts deleted file mode 100644 index 5d5074c55c41..000000000000 --- a/packages/node/src/integrations/tracing/mongo/vendored/types.ts +++ /dev/null @@ -1,70 +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-mongodb - * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 - */ -/* eslint-disable */ - -import { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { Span } from '@opentelemetry/api'; - -export interface MongoDBInstrumentationExecutionResponseHook { - (span: Span, responseInfo: MongoResponseHookInformation): void; -} - -/** - * Function that can be used to serialize db.statement tag - * @param cmd - MongoDB command object - * - * @returns serialized string that will be used as the db.statement attribute. - */ -export type DbStatementSerializer = (cmd: Record) => string; - -export interface MongoDBInstrumentationConfig extends InstrumentationConfig { - /** - * If true, additional information about query parameters and - * results will be attached (as `attributes`) to spans representing - * database operations. - */ - enhancedDatabaseReporting?: boolean; - - /** - * Hook that allows adding custom span attributes based on the data - * returned from MongoDB actions. - * - * @default undefined - */ - responseHook?: MongoDBInstrumentationExecutionResponseHook; - - /** - * Custom serializer function for the db.statement tag - */ - dbStatementSerializer?: DbStatementSerializer; - - /** - * Require parent to create mongodb span, default when unset is true - */ - requireParentSpan?: boolean; -} - -export interface MongoResponseHookInformation { - data: CommandResult; -} - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/connection/command_result.js -export type CommandResult = { - result?: unknown; - connection?: unknown; - message?: unknown; -}; - -export enum MongodbCommandType { - CREATE_INDEXES = 'createIndexes', - FIND_AND_MODIFY = 'findAndModify', - IS_MASTER = 'isMaster', - COUNT = 'count', - UNKNOWN = 'unknown', -} diff --git a/packages/node/src/integrations/tracing/mongo/vendored/utils.ts b/packages/node/src/integrations/tracing/mongo/vendored/utils.ts new file mode 100644 index 000000000000..890e5f20009b --- /dev/null +++ b/packages/node/src/integrations/tracing/mongo/vendored/utils.ts @@ -0,0 +1,240 @@ +/* + * 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-mongodb + * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs + */ + +import { SpanKind } from '@opentelemetry/api'; +import type { Span, SpanAttributes } from '@sentry/core'; +import { + getActiveSpan, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, + startInactiveSpan, + withActiveSpan, +} from '@sentry/core'; +import { + ATTR_DB_CONNECTION_STRING, + ATTR_DB_MONGODB_COLLECTION, + ATTR_DB_NAME, + ATTR_DB_OPERATION, + ATTR_DB_STATEMENT, + ATTR_DB_SYSTEM, + ATTR_NET_PEER_NAME, + ATTR_NET_PEER_PORT, + DB_SYSTEM_VALUE_MONGODB, +} from './semconv'; +import type { MongodbNamespace, MongoInternalCommand, MongoInternalTopology } from './internal-types'; +import { MongodbCommandType } from './internal-types'; + +const ORIGIN = 'auto.db.otel.mongo'; + +/** + * Replaces values in the command object with '?', hiding PII and helping grouping. + */ +function serializeDbStatement(commandObj: Record): string { + return JSON.stringify(scrubStatement(commandObj)); +} + +function scrubStatement(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(element => scrubStatement(element)); + } + + if (isCommandObj(value)) { + const initial: Record = {}; + return Object.entries(value) + .map(([key, element]) => [key, scrubStatement(element)]) + .reduce((prev, current) => { + if (isCommandEntry(current)) { + prev[current[0]] = current[1]; + } + return prev; + }, initial); + } + + // A value like string or number, possibly contains PII, scrub it + return '?'; +} + +function isCommandObj(value: Record | unknown): value is Record { + return typeof value === 'object' && value !== null && !isBuffer(value); +} + +function isBuffer(value: unknown): boolean { + return typeof Buffer !== 'undefined' && Buffer.isBuffer(value); +} + +function isCommandEntry(value: [string, unknown] | unknown): value is [string, unknown] { + return Array.isArray(value); +} + +/** + * Get the mongodb command type from the object. + */ +export function getCommandType(command: MongoInternalCommand): MongodbCommandType { + if (command.createIndexes !== undefined) { + return MongodbCommandType.CREATE_INDEXES; + } else if (command.findandmodify !== undefined) { + return MongodbCommandType.FIND_AND_MODIFY; + } else if (command.ismaster !== undefined) { + return MongodbCommandType.IS_MASTER; + } else if (command.count !== undefined) { + return MongodbCommandType.COUNT; + } else if (command.aggregate !== undefined) { + return MongodbCommandType.AGGREGATE; + } else { + return MongodbCommandType.UNKNOWN; + } +} + +/** + * Determine a span's attributes by fetching related metadata from the v4 connection context. + */ +export function getV4SpanAttributes( + connectionCtx: any, + ns: MongodbNamespace, + command?: any, + operation?: string, +): SpanAttributes { + let host, port: undefined | string; + if (connectionCtx) { + const hostParts = typeof connectionCtx.address === 'string' ? connectionCtx.address.split(':') : ''; + if (hostParts.length === 2) { + host = hostParts[0]; + port = hostParts[1]; + } + } + let commandObj: Record; + if (command?.documents && command.documents[0]) { + commandObj = command.documents[0]; + } else if (command?.cursors) { + commandObj = command.cursors; + } else { + commandObj = command; + } + + return getSpanAttributes(ns.db, ns.collection, host, port, commandObj, operation); +} + +/** + * Determine a span's attributes by fetching related metadata from the v3 topology. + */ +export function getV3SpanAttributes( + ns: string, + topology: MongoInternalTopology, + command?: MongoInternalCommand, + operation?: string | undefined, +): SpanAttributes { + let host: undefined | string; + let port: undefined | string; + if (topology?.s) { + host = topology.s.options?.host ?? topology.s.host; + port = (topology.s.options?.port ?? topology.s.port)?.toString(); + if (host == null || port == null) { + const address = topology.description?.address; + if (address) { + const addressSegments = address.split(':'); + host = addressSegments[0]; + port = addressSegments[1]; + } + } + } + + // The namespace is a combination of the database name and the name of the + // collection or index, like so: [database-name].[collection-or-index-name]. + // It could be a string or an instance of MongoDBNamespace, as such we + // always coerce to a string to extract db and collection. + const [dbName, dbCollection] = ns.toString().split('.'); + const commandObj = command?.query ?? command?.q ?? command; + + return getSpanAttributes(dbName, dbCollection, host, port, commandObj, operation); +} + +function getSpanAttributes( + dbName?: string, + dbCollection?: string, + host?: undefined | string, + port?: undefined | string, + commandObj?: any, + operation?: string | undefined, +): SpanAttributes { + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + // eslint-disable-next-line typescript/no-deprecated + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MONGODB, + // eslint-disable-next-line typescript/no-deprecated + [ATTR_DB_NAME]: dbName, + // eslint-disable-next-line typescript/no-deprecated + [ATTR_DB_MONGODB_COLLECTION]: dbCollection, + // eslint-disable-next-line typescript/no-deprecated + [ATTR_DB_OPERATION]: operation, + // eslint-disable-next-line typescript/no-deprecated + [ATTR_DB_CONNECTION_STRING]: `mongodb://${host}:${port}/${dbName}`, + }; + + if (host && port) { + // eslint-disable-next-line typescript/no-deprecated + attributes[ATTR_NET_PEER_NAME] = host; + const portNumber = parseInt(port, 10); + if (!isNaN(portNumber)) { + // eslint-disable-next-line typescript/no-deprecated + attributes[ATTR_NET_PEER_PORT] = portNumber; + } + } + + if (commandObj) { + try { + // eslint-disable-next-line typescript/no-deprecated + attributes[ATTR_DB_STATEMENT] = serializeDbStatement(commandObj); + } catch { + // ignore serialization errors — the statement is best-effort metadata + } + } + + return attributes; +} + +export function startMongoSpan(attributes: SpanAttributes): Span { + return startInactiveSpan({ + // eslint-disable-next-line typescript/no-deprecated + name: `mongodb.${attributes[ATTR_DB_OPERATION] || 'command'}`, + kind: SpanKind.CLIENT, + attributes, + }); +} + +/** + * Wraps the result handler so it ends the span (with error status on failure) and runs the + * original callback re-activated under the parent span — mongodb loses the async context when + * it invokes the callback on a later tick. + */ +export function patchEnd(span: Span | undefined, resultHandler: Function): Function { + const parentSpan = getActiveSpan(); + let spanEnded = false; + + return function patchedEnd(this: {}, ...args: unknown[]) { + if (!spanEnded) { + spanEnded = true; + const error = args[0]; + if (span) { + if (error instanceof Error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: error.message }); + } + span.end(); + } + } + + return withActiveSpan(parentSpan ?? null, () => resultHandler.apply(this, args)); + }; +} + +// The instrumentation only creates spans when there is an active parent span, to avoid emitting +// orphaned mongodb spans. +export function shouldSkipInstrumentation(): boolean { + return !getActiveSpan(); +} diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts deleted file mode 100644 index d6f6cee965f6..000000000000 --- a/packages/node/test/integrations/tracing/mongo.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MongoDBInstrumentation } from '../../../src/integrations/tracing/mongo/vendored/instrumentation'; -import { INSTRUMENTED } from '@sentry/node-core'; -import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; -import { - _defaultDbStatementSerializer, - instrumentMongo, - mongoIntegration, -} from '../../../src/integrations/tracing/mongo'; - -vi.mock('../../../src/integrations/tracing/mongo/vendored/instrumentation'); - -describe('Mongo', () => { - beforeEach(() => { - vi.clearAllMocks(); - delete INSTRUMENTED.Mongo; - - (MongoDBInstrumentation as unknown as MockInstance).mockImplementation(() => { - return { - setTracerProvider: () => undefined, - setMeterProvider: () => undefined, - getConfig: () => ({}), - setConfig: () => ({}), - enable: () => undefined, - }; - }); - }); - - it('defaults are correct for instrumentMongo', () => { - instrumentMongo(); - - expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); - expect(MongoDBInstrumentation).toHaveBeenCalledWith({ - dbStatementSerializer: expect.any(Function), - responseHook: expect.any(Function), - }); - }); - - it('defaults are correct for mongoIntegration', () => { - mongoIntegration().setupOnce!(); - - expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); - expect(MongoDBInstrumentation).toHaveBeenCalledWith({ - responseHook: expect.any(Function), - dbStatementSerializer: expect.any(Function), - }); - }); - - describe('_defaultDbStatementSerializer', () => { - it('rewrites strings as ?', () => { - const serialized = _defaultDbStatementSerializer({ - find: 'foo', - }); - expect(JSON.parse(serialized).find).toBe('?'); - }); - - it('rewrites nested strings as ?', () => { - const serialized = _defaultDbStatementSerializer({ - find: { - inner: 'foo', - }, - }); - expect(JSON.parse(serialized).find.inner).toBe('?'); - }); - - it('rewrites Buffer as ?', () => { - const serialized = _defaultDbStatementSerializer({ - find: Buffer.from('foo', 'utf8'), - }); - expect(JSON.parse(serialized).find).toBe('?'); - }); - }); -});