UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

372 lines (369 loc) 15.7 kB
import { trace, context, SpanKind } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import { getAttributesFromCollection, handleCallbackResponse, handlePromiseResponse } from './utils.js'; import { InstrumentationBase, semconvStabilityFromStr, InstrumentationNodeModuleDefinition, SemconvStability } from '@opentelemetry/instrumentation'; import { SDK_VERSION } from '@sentry/core'; import { ATTR_DB_STATEMENT, ATTR_DB_OPERATION, ATTR_DB_SYSTEM, DB_SYSTEM_NAME_VALUE_MONGODB } from './semconv.js'; import { ATTR_DB_QUERY_TEXT, ATTR_DB_OPERATION_NAME, 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) { 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) { return moduleVersion && (moduleVersion.startsWith("5.") || moduleVersion.startsWith("6.")) || false; } function needsDocumentMethodPatch(moduleVersion) { if (!moduleVersion || !moduleVersion.startsWith("8.")) { return false; } const minor = parseInt(moduleVersion.split(".")[1], 10); return minor >= 21; } const _STORED_PARENT_SPAN = /* @__PURE__ */ Symbol("stored-parent-span"); const _ALREADY_INSTRUMENTED = /* @__PURE__ */ Symbol("already-instrumented"); class MongooseInstrumentation extends InstrumentationBase { constructor(config = {}) { super(PACKAGE_NAME, SDK_VERSION, config); this._setSemconvStabilityFromEnv(); } // Used for testing. _setSemconvStabilityFromEnv() { this._netSemconvStability = semconvStabilityFromStr("http", process.env.OTEL_SEMCONV_STABILITY_OPT_IN); this._dbSemconvStability = semconvStabilityFromStr("database", process.env.OTEL_SEMCONV_STABILITY_OPT_IN); } init() { const module = new InstrumentationNodeModuleDefinition( "mongoose", [">=5.9.7 <10"], this.patch.bind(this), this.unpatch.bind(this) ); return module; } patch(module, moduleVersion) { const moduleExports = module[Symbol.toStringTag] === "Module" ? module.default : module; this._wrap(moduleExports.Model.prototype, "save", this.patchOnModelMethods("save", moduleVersion)); moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; if (instrumentRemove(moduleVersion)) { this._wrap(moduleExports.Model.prototype, "remove", this.patchOnModelMethods("remove", moduleVersion)); } 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) => { this._wrap(moduleExports.Query.prototype, funcName, 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; } unpatch(module, moduleVersion) { const moduleExports = module[Symbol.toStringTag] === "Module" ? module.default : module; const contextCaptureFunctions = getContextCaptureFunctions(moduleVersion); this._unwrap(moduleExports.Model.prototype, "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) => { this._unwrap(moduleExports.Query.prototype, funcName); }); this._unwrap(moduleExports.Model, "aggregate"); this._unwrap(moduleExports.Model, "insertMany"); this._unwrap(moduleExports.Model, "bulkWrite"); } patchAggregateExec(moduleVersion) { const self = this; return (originalAggregate) => { return function exec(callback) { if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === void 0) { return originalAggregate.apply(this, arguments); } const parentSpan = this[_STORED_PARENT_SPAN]; const 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); }; }; } patchQueryExec(moduleVersion) { const self = this; return (originalExec) => { return function exec(callback) { if (this[_ALREADY_INSTRUMENTED]) { return originalExec.apply(this, arguments); } if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === void 0) { return originalExec.apply(this, arguments); } const parentSpan = this[_STORED_PARENT_SPAN]; const 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); }; }; } patchOnModelMethods(op, moduleVersion) { const self = this; return (originalOnModelFunction) => { return function method(options, callback) { if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === void 0) { return originalOnModelFunction.apply(this, arguments); } const serializePayload = { document: this }; if (options && !(options instanceof Function)) { serializePayload.options = options; } const 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 = void 0; } return self._handleResponse(span, originalOnModelFunction, this, arguments, callback, moduleVersion); }; }; } // Patch document instance methods (doc.updateOne/deleteOne) for Mongoose 8.21.0+. _patchDocumentUpdateMethods(op, moduleVersion) { const self = this; return (originalMethod) => { return function method(update, options, callback) { if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === void 0) { return originalMethod.apply(this, arguments); } let actualCallback = callback; let actualUpdate = update; let actualOptions = options; if (typeof update === "function") { actualCallback = update; actualUpdate = void 0; actualOptions = void 0; } else if (typeof options === "function") { actualCallback = options; actualOptions = void 0; } const 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); if (result && typeof result === "object") { result[_ALREADY_INSTRUMENTED] = true; } return result; }; }; } patchModelStatic(op, moduleVersion) { const self = this; return (original) => { return function patchedStatic(docsOrOps, options, callback) { if (self.getConfig().requireParentSpan && trace.getSpan(context.active()) === void 0) { return original.apply(this, arguments); } if (typeof options === "function") { callback = options; options = void 0; } const serializePayload = {}; switch (op) { case "insertMany": serializePayload.documents = docsOrOps; break; case "bulkWrite": serializePayload.operations = docsOrOps; break; default: serializePayload.document = docsOrOps; break; } if (options !== void 0) { serializePayload.options = options; } const 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 patchModelAggregate() { const self = this; return (original) => { return function captureSpanContext() { const currentSpan = trace.getSpan(context.active()); const aggregate = self._callOriginalFunction(() => original.apply(this, arguments)); if (aggregate) aggregate[_STORED_PARENT_SPAN] = currentSpan; return aggregate; }; }; } patchAndCaptureSpanContext(funcName) { const self = this; return (original) => { return function captureSpanContext() { this[_STORED_PARENT_SPAN] = trace.getSpan(context.active()); return self._callOriginalFunction(() => original.apply(this, arguments)); }; }; } _startSpan(collection, modelName, operation, attributes, parentSpan) { const finalAttributes = { ...attributes, ...getAttributesFromCollection(collection, this._dbSemconvStability, this._netSemconvStability) }; if (this._dbSemconvStability & SemconvStability.OLD) { finalAttributes[ATTR_DB_OPERATION] = operation; finalAttributes[ATTR_DB_SYSTEM] = "mongoose"; } if (this._dbSemconvStability & SemconvStability.STABLE) { finalAttributes[ATTR_DB_OPERATION_NAME] = operation; finalAttributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MONGODB; } 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) : void 0 ); } _handleResponse(span, exec, originalThis, args, callback, moduleVersion = void 0) { 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); } } _callOriginalFunction(originalFunction) { if (this.getConfig().suppressInternalInstrumentation) { return context.with(suppressTracing(context.active()), originalFunction); } else { return originalFunction(); } } } export { MongooseInstrumentation, _ALREADY_INSTRUMENTED, _STORED_PARENT_SPAN }; //# sourceMappingURL=mongoose.js.map