diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index bbad1d8218c2..a3199de2d8f3 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -154,6 +154,7 @@ "**/nestjs/src/integrations/vendored/**/*.ts", "**/integrations/tracing/kafka/vendored/**/*.ts", "**/integrations/tracing/tedious/vendored/**/*.ts", + "**/integrations/tracing/mongoose/vendored/**/*.ts", "**/integrations/tracing/amqplib/vendored/**/*.ts" ], "rules": { diff --git a/packages/node/package.json b/packages/node/package.json index 9ec779e57d0b..db39414bf525 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -71,7 +71,6 @@ "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", - "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/sql-common": "^0.41.2", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/sdk-trace-base": "^2.6.1", diff --git a/packages/node/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose/index.ts similarity index 93% rename from packages/node/src/integrations/tracing/mongoose.ts rename to packages/node/src/integrations/tracing/mongoose/index.ts index 8e81e43b83d7..811eb0bd7905 100644 --- a/packages/node/src/integrations/tracing/mongoose.ts +++ b/packages/node/src/integrations/tracing/mongoose/index.ts @@ -1,4 +1,4 @@ -import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; +import { MongooseInstrumentation } from './vendored/mongoose'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts new file mode 100644 index 000000000000..5192316d5d0c --- /dev/null +++ b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts @@ -0,0 +1,39 @@ +/* + * Simplified type definitions vendored from mongoose. + * Only includes the types actually accessed by the instrumentation. + */ +/* eslint-disable */ + +export interface Collection { + name: string; + conn: Connection; + [key: string]: any; +} + +export interface Connection { + name: string; + host: string; + port: number; + user?: string; + [key: string]: any; +} + +export declare const Model: { + prototype: any; + collection: Collection; + modelName: string; + aggregate: Function; + insertMany: Function; + bulkWrite: Function; + [key: string]: any; +}; + +export declare const Query: { + prototype: any; + [key: string]: any; +}; + +export declare const Aggregate: { + prototype: any; + [key: string]: any; +}; diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts new file mode 100644 index 000000000000..8ebf9bc15bdf --- /dev/null +++ b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts @@ -0,0 +1,498 @@ +/* + * 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-mongoose + * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 + * - Types vendored from mongoose as simplified interfaces + * - Minor TypeScript strictness adjustments for this repository's compiler settings + */ +/* eslint-disable */ + +import { context, Span, trace, Attributes, SpanKind } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import type * as mongoose from './mongoose-types'; +import { MongooseInstrumentationConfig, SerializerPayload } from './types'; +import { handleCallbackResponse, handlePromiseResponse, getAttributesFromCollection } from './utils'; +import { + InstrumentationBase, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, + SemconvStability, + semconvStabilityFromStr, +} from '@opentelemetry/instrumentation'; +import { SDK_VERSION } from '@sentry/core'; +import { ATTR_DB_OPERATION, ATTR_DB_STATEMENT, ATTR_DB_SYSTEM, DB_SYSTEM_NAME_VALUE_MONGODB } from './semconv'; +import { ATTR_DB_OPERATION_NAME, ATTR_DB_QUERY_TEXT, ATTR_DB_SYSTEM_NAME } from '@opentelemetry/semantic-conventions'; + +const PACKAGE_NAME = '@sentry/instrumentation-mongoose'; + +const contextCaptureFunctionsCommon = [ + 'deleteOne', + 'deleteMany', + 'find', + 'findOne', + 'estimatedDocumentCount', + 'countDocuments', + 'distinct', + 'where', + '$where', + 'findOneAndUpdate', + 'findOneAndDelete', + 'findOneAndReplace', +]; + +const contextCaptureFunctions6 = ['remove', 'count', 'findOneAndRemove', ...contextCaptureFunctionsCommon]; +const contextCaptureFunctions7 = ['count', 'findOneAndRemove', ...contextCaptureFunctionsCommon]; +const contextCaptureFunctions8 = [...contextCaptureFunctionsCommon]; + +function getContextCaptureFunctions(moduleVersion: string | undefined): string[] { + /* istanbul ignore next */ + if (!moduleVersion) { + return contextCaptureFunctionsCommon; + } else if (moduleVersion.startsWith('6.') || moduleVersion.startsWith('5.')) { + return contextCaptureFunctions6; + } else if (moduleVersion.startsWith('7.')) { + return contextCaptureFunctions7; + } else { + return contextCaptureFunctions8; + } +} + +function instrumentRemove(moduleVersion: string | undefined): boolean { + return (moduleVersion && (moduleVersion.startsWith('5.') || moduleVersion.startsWith('6.'))) || false; +} + +/** + * 8.21.0 changed Document.updateOne/deleteOne so that the Query is not fully built when Query.exec() is called. + * @param moduleVersion + */ +function needsDocumentMethodPatch(moduleVersion: string | undefined): boolean { + if (!moduleVersion || !moduleVersion.startsWith('8.')) { + return false; + } + + const minor = parseInt(moduleVersion.split('.')[1]!, 10); + return minor >= 21; +} + +// when mongoose functions are called, we store the original call context +// and then set it as the parent for the spans created by Query/Aggregate exec() +// calls. this bypass the unlinked spans issue on thenables await operations. +export const _STORED_PARENT_SPAN: unique symbol = Symbol('stored-parent-span'); + +// Prevents double-instrumentation when doc.updateOne/deleteOne (Mongoose 8.21.0+) +// creates a span and returns a Query that also calls exec() +export const _ALREADY_INSTRUMENTED: unique symbol = Symbol('already-instrumented'); + +export class MongooseInstrumentation extends InstrumentationBase { + private _netSemconvStability!: SemconvStability; + private _dbSemconvStability!: SemconvStability; + + constructor(config: MongooseInstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, 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); + } + + protected init(): InstrumentationModuleDefinition { + const module = new InstrumentationNodeModuleDefinition( + 'mongoose', + ['>=5.9.7 <10'], + this.patch.bind(this), + this.unpatch.bind(this), + ); + return module; + } + + private patch(module: any, moduleVersion: string | undefined) { + const moduleExports: typeof mongoose = module[Symbol.toStringTag] === 'Module' ? module.default : module; + + this._wrap(moduleExports.Model.prototype, 'save', this.patchOnModelMethods('save', moduleVersion)); + // mongoose applies this code on module require: + // Model.prototype.$save = Model.prototype.save; + // which captures the save function before it is patched. + // so we need to apply the same logic after instrumenting the save function. + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + + if (instrumentRemove(moduleVersion)) { + this._wrap(moduleExports.Model.prototype, 'remove', this.patchOnModelMethods('remove', moduleVersion)); + } + + // Mongoose 8.21.0+ changed Document.updateOne()/deleteOne() so that the Query is not fully built when Query.exec() is called. + if (needsDocumentMethodPatch(moduleVersion)) { + this._wrap( + moduleExports.Model.prototype, + 'updateOne', + this._patchDocumentUpdateMethods('updateOne', moduleVersion), + ); + this._wrap( + moduleExports.Model.prototype, + 'deleteOne', + this._patchDocumentUpdateMethods('deleteOne', moduleVersion), + ); + } + + this._wrap(moduleExports.Query.prototype, 'exec', this.patchQueryExec(moduleVersion)); + this._wrap(moduleExports.Aggregate.prototype, 'exec', this.patchAggregateExec(moduleVersion)); + + const contextCaptureFunctions = getContextCaptureFunctions(moduleVersion); + + contextCaptureFunctions.forEach((funcName: string) => { + this._wrap(moduleExports.Query.prototype, funcName as any, this.patchAndCaptureSpanContext(funcName)); + }); + this._wrap(moduleExports.Model, 'aggregate', this.patchModelAggregate()); + + this._wrap(moduleExports.Model, 'insertMany', this.patchModelStatic('insertMany', moduleVersion)); + this._wrap(moduleExports.Model, 'bulkWrite', this.patchModelStatic('bulkWrite', moduleVersion)); + + return moduleExports; + } + + private unpatch(module: any, moduleVersion: string | undefined): void { + const moduleExports: typeof mongoose = module[Symbol.toStringTag] === 'Module' ? module.default : module; + + const contextCaptureFunctions = getContextCaptureFunctions(moduleVersion); + + this._unwrap(moduleExports.Model.prototype, 'save'); + // revert the patch for $save which we applied by aliasing it to patched `save` + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + + if (instrumentRemove(moduleVersion)) { + this._unwrap(moduleExports.Model.prototype, 'remove'); + } + + if (needsDocumentMethodPatch(moduleVersion)) { + this._unwrap(moduleExports.Model.prototype, 'updateOne'); + this._unwrap(moduleExports.Model.prototype, 'deleteOne'); + } + + this._unwrap(moduleExports.Query.prototype, 'exec'); + this._unwrap(moduleExports.Aggregate.prototype, 'exec'); + + contextCaptureFunctions.forEach((funcName: string) => { + this._unwrap(moduleExports.Query.prototype, funcName as any); + }); + this._unwrap(moduleExports.Model, 'aggregate'); + + this._unwrap(moduleExports.Model, 'insertMany'); + this._unwrap(moduleExports.Model, 'bulkWrite'); + } + + private patchAggregateExec(moduleVersion: string | undefined) { + const self = this; + return (originalAggregate: Function) => { + return function exec(this: any, callback?: Function) { + if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === undefined) { + return originalAggregate.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: Attributes = {}; + const { dbStatementSerializer } = self.getConfig(); + if (dbStatementSerializer) { + const statement = dbStatementSerializer('aggregate', { + options: this.options, + aggregatePipeline: this._pipeline, + }); + if (self._dbSemconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = statement; + } + if (self._dbSemconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = statement; + } + } + + const span = self._startSpan( + this._model.collection, + this._model?.modelName, + 'aggregate', + attributes, + parentSpan, + ); + + return self._handleResponse(span, originalAggregate, this, arguments, callback, moduleVersion); + }; + }; + } + + private patchQueryExec(moduleVersion: string | undefined) { + const self = this; + return (originalExec: Function) => { + return function exec(this: any, callback?: Function) { + // Skip if already instrumented by document instance method patch + if (this[_ALREADY_INSTRUMENTED]) { + return originalExec.apply(this, arguments); + } + + if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === undefined) { + return originalExec.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: Attributes = {}; + const { dbStatementSerializer } = self.getConfig(); + if (dbStatementSerializer) { + const statement = dbStatementSerializer(this.op, { + // Use public API methods (getFilter/getOptions) for better compatibility + condition: this.getFilter?.() ?? this._conditions, + updates: this._update, + options: this.getOptions?.() ?? this.options, + fields: this._fields, + }); + if (self._dbSemconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = statement; + } + if (self._dbSemconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = statement; + } + } + const span = self._startSpan(this.mongooseCollection, this.model.modelName, this.op, attributes, parentSpan); + + return self._handleResponse(span, originalExec, this, arguments, callback, moduleVersion); + }; + }; + } + + private patchOnModelMethods(op: string, moduleVersion: string | undefined) { + const self = this; + return (originalOnModelFunction: Function) => { + return function method(this: any, options?: any, callback?: Function) { + if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === undefined) { + return originalOnModelFunction.apply(this, arguments); + } + + const serializePayload: SerializerPayload = { document: this }; + if (options && !(options instanceof Function)) { + serializePayload.options = options; + } + const attributes: Attributes = {}; + const { dbStatementSerializer } = self.getConfig(); + if (dbStatementSerializer) { + const statement = dbStatementSerializer(op, serializePayload); + if (self._dbSemconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = statement; + } + if (self._dbSemconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = statement; + } + } + const span = self._startSpan(this.constructor.collection, this.constructor.modelName, op, attributes); + + if (options instanceof Function) { + callback = options; + options = undefined; + } + + return self._handleResponse(span, originalOnModelFunction, this, arguments, callback, moduleVersion); + }; + }; + } + + // Patch document instance methods (doc.updateOne/deleteOne) for Mongoose 8.21.0+. + private _patchDocumentUpdateMethods(op: string, moduleVersion: string | undefined) { + const self = this; + return (originalMethod: Function) => { + return function method(this: any, update?: any, options?: any, callback?: Function) { + if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === undefined) { + return originalMethod.apply(this, arguments); + } + + // determine actual callback since different argument patterns are allowed + let actualCallback: Function | undefined = callback; + let actualUpdate = update; + let actualOptions = options; + + if (typeof update === 'function') { + actualCallback = update; + actualUpdate = undefined; + actualOptions = undefined; + } else if (typeof options === 'function') { + actualCallback = options; + actualOptions = undefined; + } + + const attributes: Attributes = {}; + const dbStatementSerializer = self.getConfig().dbStatementSerializer; + if (dbStatementSerializer) { + const statement = dbStatementSerializer(op, { + // Document instance methods automatically use the document's _id as filter + condition: { _id: this._id }, + updates: actualUpdate, + options: actualOptions, + }); + if (self._dbSemconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = statement; + } + if (self._dbSemconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = statement; + } + } + + const span = self._startSpan(this.constructor.collection, this.constructor.modelName, op, attributes); + + const result = self._handleResponse(span, originalMethod, this, arguments, actualCallback, moduleVersion); + + // Mark returned Query to prevent double-instrumentation when exec() is eventually called + if (result && typeof result === 'object') { + result[_ALREADY_INSTRUMENTED] = true; + } + + return result; + }; + }; + } + + private patchModelStatic(op: string, moduleVersion: string | undefined) { + const self = this; + return (original: Function) => { + return function patchedStatic(this: any, docsOrOps: any, options?: any, callback?: Function) { + if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === undefined) { + return original.apply(this, arguments); + } + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + const serializePayload: SerializerPayload = {}; + switch (op) { + case 'insertMany': + serializePayload.documents = docsOrOps; + break; + case 'bulkWrite': + serializePayload.operations = docsOrOps; + break; + default: + serializePayload.document = docsOrOps; + break; + } + if (options !== undefined) { + serializePayload.options = options; + } + + const attributes: Attributes = {}; + const { dbStatementSerializer } = self.getConfig(); + if (dbStatementSerializer) { + const statement = dbStatementSerializer(op, serializePayload); + if (self._dbSemconvStability & SemconvStability.OLD) { + attributes[ATTR_DB_STATEMENT] = statement; + } + if (self._dbSemconvStability & SemconvStability.STABLE) { + attributes[ATTR_DB_QUERY_TEXT] = statement; + } + } + + const span = self._startSpan(this.collection, this.modelName, op, attributes); + + return self._handleResponse(span, original, this, arguments, callback, moduleVersion); + }; + }; + } + + // we want to capture the otel span on the object which is calling exec. + // in the special case of aggregate, we need have no function to path + // on the Aggregate object to capture the context on, so we patch + // the aggregate of Model, and set the context on the Aggregate object + private patchModelAggregate() { + const self = this; + return (original: Function) => { + return function captureSpanContext(this: any) { + const currentSpan = trace.getSpan(context.active()); + const aggregate = self._callOriginalFunction(() => original.apply(this, arguments)); + if (aggregate) aggregate[_STORED_PARENT_SPAN] = currentSpan; + return aggregate; + }; + }; + } + + private patchAndCaptureSpanContext(funcName: string) { + const self = this; + return (original: Function) => { + return function captureSpanContext(this: any) { + this[_STORED_PARENT_SPAN] = trace.getSpan(context.active()); + return self._callOriginalFunction(() => original.apply(this, arguments)); + }; + }; + } + + private _startSpan( + collection: mongoose.Collection, + modelName: string, + operation: string, + attributes: Attributes, + parentSpan?: Span, + ): Span { + const finalAttributes: Attributes = { + ...attributes, + ...getAttributesFromCollection(collection, this._dbSemconvStability, this._netSemconvStability), + }; + + if (this._dbSemconvStability & SemconvStability.OLD) { + finalAttributes[ATTR_DB_OPERATION] = operation; + finalAttributes[ATTR_DB_SYSTEM] = 'mongoose'; // keep for backwards compatibility + } + if (this._dbSemconvStability & SemconvStability.STABLE) { + finalAttributes[ATTR_DB_OPERATION_NAME] = operation; + finalAttributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MONGODB; // actual db system name + } + + const spanName = + this._dbSemconvStability & SemconvStability.STABLE + ? `${operation} ${collection.name}` + : `mongoose.${modelName}.${operation}`; + + return this.tracer.startSpan( + spanName, + { + kind: SpanKind.CLIENT, + attributes: finalAttributes, + }, + parentSpan ? trace.setSpan(context.active(), parentSpan) : undefined, + ); + } + + private _handleResponse( + span: Span, + exec: Function, + originalThis: any, + args: IArguments, + callback?: Function, + moduleVersion: string | undefined = undefined, + ) { + const self = this; + if (callback instanceof Function) { + return self._callOriginalFunction(() => + handleCallbackResponse(callback, exec, originalThis, span, args, self.getConfig().responseHook, moduleVersion), + ); + } else { + const response = self._callOriginalFunction(() => exec.apply(originalThis, args)); + return handlePromiseResponse(response, span, self.getConfig().responseHook, moduleVersion); + } + } + + private _callOriginalFunction(originalFunction: (...args: any[]) => T): T { + if (this.getConfig().suppressInternalInstrumentation) { + return context.with(suppressTracing(context.active()), originalFunction); + } else { + return originalFunction(); + } + } +} diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/semconv.ts b/packages/node/src/integrations/tracing/mongoose/vendored/semconv.ts new file mode 100644 index 000000000000..db82a1d7d237 --- /dev/null +++ b/packages/node/src/integrations/tracing/mongoose/vendored/semconv.ts @@ -0,0 +1,99 @@ +/* + * 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-mongoose + * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 + */ +/* eslint-disable */ + +/** + * Deprecated, use `db.collection.name` instead. + * + * @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; + +/** + * Deprecated, use `db.namespace` instead. + * + * @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; + +/** + * Deprecated, use `db.operation.name` instead. + * + * @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; + +/** + * The database statement being executed. + * + * @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; + +/** + * 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; + +/** + * Deprecated, no replacement at this time. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Removed, no replacement at this time. + */ +export const ATTR_DB_USER = 'db.user' as const; + +/** + * Deprecated, use `server.address` on client spans and `client.address` on server spans. + * + * @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; + +/** + * Deprecated, use `server.port` on client spans and `client.port` on server spans. + * + * @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}. + * + * @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; diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/types.ts b/packages/node/src/integrations/tracing/mongoose/vendored/types.ts new file mode 100644 index 000000000000..761cb26bd8f8 --- /dev/null +++ b/packages/node/src/integrations/tracing/mongoose/vendored/types.ts @@ -0,0 +1,64 @@ +/* + * 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-mongoose + * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 + */ +/* eslint-disable */ + +import { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface SerializerPayload { + condition?: any; + options?: any; + updates?: any; + document?: any; + aggregatePipeline?: any; + fields?: any; + documents?: any; + operations?: any; +} + +export type DbStatementSerializer = (operation: string, payload: SerializerPayload) => string; + +export interface ResponseInfo { + moduleVersion: string | undefined; + response: any; +} + +export type MongooseResponseCustomAttributesFunction = (span: Span, responseInfo: ResponseInfo) => void; + +export interface MongooseInstrumentationConfig extends InstrumentationConfig { + /** + * Mongoose operation use mongodb under the hood. + * If mongodb instrumentation is enabled, a mongoose operation will also create + * a mongodb operation describing the communication with mongoDB servers. + * Setting the `suppressInternalInstrumentation` config value to `true` will + * cause the instrumentation to suppress instrumentation of underlying operations, + * effectively causing mongodb spans to be non-recordable. + */ + suppressInternalInstrumentation?: boolean; + + /** Custom serializer function for the db.statement tag */ + dbStatementSerializer?: DbStatementSerializer; + + /** hook for adding custom attributes using the response payload */ + responseHook?: MongooseResponseCustomAttributesFunction; + + /** Set to true if you do not want to collect traces that start with mongoose */ + requireParentSpan?: boolean; +} diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts b/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts new file mode 100644 index 000000000000..8e4824380404 --- /dev/null +++ b/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts @@ -0,0 +1,152 @@ +/* + * 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-mongoose + * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 + * - Types vendored from mongoose as simplified interfaces + */ +/* eslint-disable */ + +import { Attributes, SpanStatusCode, diag, Span } from '@opentelemetry/api'; +import type { Collection } from './mongoose-types'; +import { MongooseResponseCustomAttributesFunction } from './types'; +import { safeExecuteInTheMiddle, SemconvStability } from '@opentelemetry/instrumentation'; +import { + ATTR_DB_MONGODB_COLLECTION, + ATTR_DB_NAME, + ATTR_DB_USER, + ATTR_NET_PEER_NAME, + ATTR_NET_PEER_PORT, +} from './semconv'; +import { + ATTR_DB_COLLECTION_NAME, + ATTR_DB_NAMESPACE, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} from '@opentelemetry/semantic-conventions'; + +export function getAttributesFromCollection( + collection: Collection, + dbSemconvStability: SemconvStability, + netSemconvStability: SemconvStability, +): Attributes { + const attrs: Attributes = {}; + + if (dbSemconvStability & SemconvStability.OLD) { + attrs[ATTR_DB_MONGODB_COLLECTION] = collection.name; + attrs[ATTR_DB_NAME] = collection.conn.name; + attrs[ATTR_DB_USER] = collection.conn.user; + } + if (dbSemconvStability & SemconvStability.STABLE) { + attrs[ATTR_DB_COLLECTION_NAME] = collection.name; + attrs[ATTR_DB_NAMESPACE] = collection.conn.name; + } + + if (netSemconvStability & SemconvStability.OLD) { + attrs[ATTR_NET_PEER_NAME] = collection.conn.host; + attrs[ATTR_NET_PEER_PORT] = collection.conn.port; + } + if (netSemconvStability & SemconvStability.STABLE) { + attrs[ATTR_SERVER_ADDRESS] = collection.conn.host; + attrs[ATTR_SERVER_PORT] = collection.conn.port; + } + + return attrs; +} + +function setErrorStatus(span: Span, error: any = {}) { + span.recordException(error); + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: `${error.message} ${error.code ? `\nMongoose Error Code: ${error.code}` : ''}`, + }); +} + +function applyResponseHook( + span: Span, + response: any, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined, +) { + if (!responseHook) { + return; + } + + safeExecuteInTheMiddle( + () => responseHook(span, { moduleVersion, response }), + e => { + if (e) { + diag.error('mongoose instrumentation: responseHook error', e); + } + }, + true, + ); +} + +export function handlePromiseResponse( + execResponse: any, + span: Span, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined, +): any { + if (!(execResponse instanceof Promise)) { + applyResponseHook(span, execResponse, responseHook, moduleVersion); + span.end(); + return execResponse; + } + + return execResponse + .then(response => { + applyResponseHook(span, response, responseHook, moduleVersion); + return response; + }) + .catch(err => { + setErrorStatus(span, err); + throw err; + }) + .finally(() => span.end()); +} + +export function handleCallbackResponse( + callback: Function, + exec: Function, + originalThis: any, + span: Span, + args: IArguments, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined, +) { + let callbackArgumentIndex = 0; + if (args.length === 2) { + callbackArgumentIndex = 1; + } else if (args.length === 3) { + callbackArgumentIndex = 2; + } + + args[callbackArgumentIndex] = (err: Error, response: any): any => { + if (err) { + setErrorStatus(span, err); + } else { + applyResponseHook(span, response, responseHook, moduleVersion); + } + + span.end(); + return callback!(err, response); + }; + + return exec.apply(originalThis, args); +} diff --git a/yarn.lock b/yarn.lock index ec5e1eb43b06..11d736b987a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6084,15 +6084,6 @@ "@opentelemetry/semantic-conventions" "^1.29.0" forwarded-parse "2.1.2" -"@opentelemetry/instrumentation-mongoose@0.60.0": - version "0.60.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz#9481a90d3f75d66244d7f63709529cb7f2823103" - integrity sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg== - dependencies: - "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.214.0" - "@opentelemetry/semantic-conventions" "^1.33.0" - "@opentelemetry/instrumentation-pg@0.66.0": version "0.66.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz#78d16b50dc4c5d851015823611a46243d63a88fb"