UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

686 lines (634 loc) 25.9 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const api = require('@opentelemetry/api'); const core = require('@sentry/core'); const instrumentation = require('@opentelemetry/instrumentation'); const semanticConventions = require('@opentelemetry/semantic-conventions'); const redisCommon = require('./redis-common.js'); const semconv = require('./semconv.js'); /* * 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/instrumentation-redis-v0.62.0/packages/instrumentation-redis * - Upstream version: @opentelemetry/instrumentation-redis@0.62.0 * - Minor TypeScript adjustments for this repository's compiler settings */ /* eslint-disable -- vendored @opentelemetry/instrumentation-redis */ const PACKAGE_NAME = '@opentelemetry/instrumentation-redis'; const PACKAGE_VERSION = '0.62.0'; // ---- Internal types ---- const OTEL_OPEN_SPANS = Symbol('opentelemetry.instrumentation.redis.open_spans'); const MULTI_COMMAND_OPTIONS = Symbol('opentelemetry.instrumentation.redis.multi_command_options'); // ---- v4-v5 utils ---- function removeCredentialsFromDBConnectionStringAttribute( diagLogger, url, ) { if (typeof url !== 'string' || !url) { return undefined; } try { const u = new URL(url); u.searchParams.delete('user_pwd'); u.username = ''; u.password = ''; return u.href; } catch (err) { diagLogger.error('failed to sanitize redis connection url', err); } return undefined; } function getClientAttributes( diagLogger, options, semconvStability, ) { const attributes = {}; if (semconvStability & instrumentation.SemconvStability.OLD) { Object.assign(attributes, { [semconv.ATTR_DB_SYSTEM]: semconv.DB_SYSTEM_VALUE_REDIS, [semconv.ATTR_NET_PEER_NAME]: options?.socket?.host, [semconv.ATTR_NET_PEER_PORT]: options?.socket?.port, [semconv.ATTR_DB_CONNECTION_STRING]: removeCredentialsFromDBConnectionStringAttribute(diagLogger, options?.url), }); } if (semconvStability & instrumentation.SemconvStability.STABLE) { Object.assign(attributes, { [semanticConventions.ATTR_DB_SYSTEM_NAME]: semconv.DB_SYSTEM_NAME_VALUE_REDIS, [semanticConventions.ATTR_SERVER_ADDRESS]: options?.socket?.host, [semanticConventions.ATTR_SERVER_PORT]: options?.socket?.port, }); } return attributes; } // ---- v2-v3 utils ---- function endSpanV2(span, err) { if (err) { span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message, }); } span.end(); } function getTracedCreateClient(original) { return function createClientTrace() { const client = original.apply(this, arguments); return api.context.bind(api.context.active(), client); }; } function getTracedCreateStreamTrace(original) { return function create_stream_trace() { if (!Object.prototype.hasOwnProperty.call(this, 'stream')) { Object.defineProperty(this, 'stream', { get() { return this._patched_redis_stream; }, set(val) { api.context.bind(api.context.active(), val); this._patched_redis_stream = val; }, }); } return original.apply(this, arguments); }; } // ---- RedisInstrumentationV2_V3 ---- class RedisInstrumentationV2_V3 extends instrumentation.InstrumentationBase { static __initStatic() {this.COMPONENT = 'redis';} constructor(config = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); this._semconvStability = config.semconvStability ? config.semconvStability : instrumentation.semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); } setConfig(config = {}) { super.setConfig(config); this._semconvStability = config.semconvStability ? config.semconvStability : instrumentation.semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); } init() { return [ new instrumentation.InstrumentationNodeModuleDefinition( 'redis', ['>=2.6.0 <4'], (moduleExports) => { if (instrumentation.isWrapped(moduleExports.RedisClient.prototype['internal_send_command'])) { this._unwrap(moduleExports.RedisClient.prototype, 'internal_send_command'); } this._wrap(moduleExports.RedisClient.prototype, 'internal_send_command', this._getPatchInternalSendCommand()); if (instrumentation.isWrapped(moduleExports.RedisClient.prototype['create_stream'])) { this._unwrap(moduleExports.RedisClient.prototype, 'create_stream'); } this._wrap(moduleExports.RedisClient.prototype, 'create_stream', this._getPatchCreateStream()); if (instrumentation.isWrapped(moduleExports.createClient)) { this._unwrap(moduleExports, 'createClient'); } this._wrap(moduleExports, 'createClient', this._getPatchCreateClient()); return moduleExports; }, (moduleExports) => { if (moduleExports === undefined) return; this._unwrap(moduleExports.RedisClient.prototype, 'internal_send_command'); this._unwrap(moduleExports.RedisClient.prototype, 'create_stream'); this._unwrap(moduleExports, 'createClient'); }, ), ]; } _getPatchInternalSendCommand() { const instrumentation$1 = this; return function internal_send_command(original) { return function internal_send_command_trace( cmd) { if (arguments.length !== 1 || typeof cmd !== 'object') { return original.apply(this, arguments); } const config = instrumentation$1.getConfig(); const hasNoParentSpan = api.trace.getSpan(api.context.active()) === undefined; if (config.requireParentSpan === true && hasNoParentSpan) { return original.apply(this, arguments); } const dbStatementSerializer = config?.dbStatementSerializer || redisCommon.defaultDbStatementSerializer; const attributes = {}; if (instrumentation$1._semconvStability & instrumentation.SemconvStability.OLD) { Object.assign(attributes, { [semconv.ATTR_DB_SYSTEM]: semconv.DB_SYSTEM_VALUE_REDIS, [semconv.ATTR_DB_STATEMENT]: dbStatementSerializer(cmd.command, cmd.args), }); } if (instrumentation$1._semconvStability & instrumentation.SemconvStability.STABLE) { Object.assign(attributes, { [semanticConventions.ATTR_DB_SYSTEM_NAME]: semconv.DB_SYSTEM_NAME_VALUE_REDIS, [semanticConventions.ATTR_DB_OPERATION_NAME]: cmd.command, [semanticConventions.ATTR_DB_QUERY_TEXT]: dbStatementSerializer(cmd.command, cmd.args), }); } attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.db.otel.redis'; const span = instrumentation$1.tracer.startSpan(`${RedisInstrumentationV2_V3.COMPONENT}-${cmd.command}`, { kind: api.SpanKind.CLIENT, attributes, }); if (this.connection_options) { const connectionAttributes = {}; if (instrumentation$1._semconvStability & instrumentation.SemconvStability.OLD) { Object.assign(connectionAttributes, { [semconv.ATTR_NET_PEER_NAME]: this.connection_options.host, [semconv.ATTR_NET_PEER_PORT]: this.connection_options.port, }); } if (instrumentation$1._semconvStability & instrumentation.SemconvStability.STABLE) { Object.assign(connectionAttributes, { [semanticConventions.ATTR_SERVER_ADDRESS]: this.connection_options.host, [semanticConventions.ATTR_SERVER_PORT]: this.connection_options.port, }); } span.setAttributes(connectionAttributes); } if (this.address && instrumentation$1._semconvStability & instrumentation.SemconvStability.OLD) { span.setAttribute(semconv.ATTR_DB_CONNECTION_STRING, `redis://${this.address}`); } const originalCallback = arguments[0].callback; if (originalCallback) { const originalContext = api.context.active(); arguments[0].callback = function callback( err, reply) { if (config?.responseHook) { const responseHook = config.responseHook; instrumentation.safeExecuteInTheMiddle( () => { responseHook(span, cmd.command, cmd.args, reply); }, (e) => { if (e) { instrumentation$1._diag.error('Error executing responseHook', e); } }, true, ); } endSpanV2(span, err); return api.context.with(originalContext, originalCallback, this, ...arguments); }; } try { return original.apply(this, arguments); } catch (rethrow) { endSpanV2(span, rethrow ); throw rethrow; } }; }; } _getPatchCreateClient() { return function createClient(original) { return getTracedCreateClient(original); }; } _getPatchCreateStream() { return function createReadStream(original) { return getTracedCreateStreamTrace(original); }; } } RedisInstrumentationV2_V3.__initStatic(); // ---- RedisInstrumentationV4_V5 ---- class RedisInstrumentationV4_V5 extends instrumentation.InstrumentationBase { static __initStatic2() {this.COMPONENT = 'redis';} constructor(config = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); this._semconvStability = config.semconvStability ? config.semconvStability : instrumentation.semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); } setConfig(config = {}) { super.setConfig(config); this._semconvStability = config.semconvStability ? config.semconvStability : instrumentation.semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); } init() { return [ this._getInstrumentationNodeModuleDefinition('@redis/client'), this._getInstrumentationNodeModuleDefinition('@node-redis/client'), ]; } _getInstrumentationNodeModuleDefinition(basePackageName) { const commanderModuleFile = new instrumentation.InstrumentationNodeModuleFile( `${basePackageName}/dist/lib/commander.js`, ['^1.0.0'], (moduleExports, moduleVersion) => { const transformCommandArguments = moduleExports.transformCommandArguments; if (!transformCommandArguments) { this._diag.error('internal instrumentation error, missing transformCommandArguments function'); return moduleExports; } const functionToPatch = moduleVersion?.startsWith('1.0.') ? 'extendWithCommands' : 'attachCommands'; if (instrumentation.isWrapped(moduleExports?.[functionToPatch])) { this._unwrap(moduleExports, functionToPatch); } this._wrap(moduleExports, functionToPatch, this._getPatchExtendWithCommands(transformCommandArguments)); return moduleExports; }, (moduleExports) => { if (instrumentation.isWrapped(moduleExports?.extendWithCommands)) { this._unwrap(moduleExports, 'extendWithCommands'); } if (instrumentation.isWrapped(moduleExports?.attachCommands)) { this._unwrap(moduleExports, 'attachCommands'); } }, ); const multiCommanderModule = new instrumentation.InstrumentationNodeModuleFile( `${basePackageName}/dist/lib/client/multi-command.js`, ['^1.0.0', '>=5.0.0 <5.12.0'], (moduleExports) => { const redisClientMultiCommandPrototype = moduleExports?.default?.prototype; if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.exec)) { this._unwrap(redisClientMultiCommandPrototype, 'exec'); } this._wrap(redisClientMultiCommandPrototype, 'exec', this._getPatchMultiCommandsExec(false)); if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.execAsPipeline)) { this._unwrap(redisClientMultiCommandPrototype, 'execAsPipeline'); } this._wrap(redisClientMultiCommandPrototype, 'execAsPipeline', this._getPatchMultiCommandsExec(true)); if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.addCommand)) { this._unwrap(redisClientMultiCommandPrototype, 'addCommand'); } this._wrap(redisClientMultiCommandPrototype, 'addCommand', this._getPatchMultiCommandsAddCommand()); return moduleExports; }, (moduleExports) => { const redisClientMultiCommandPrototype = moduleExports?.default?.prototype; if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.exec)) { this._unwrap(redisClientMultiCommandPrototype, 'exec'); } if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.execAsPipeline)) { this._unwrap(redisClientMultiCommandPrototype, 'execAsPipeline'); } if (instrumentation.isWrapped(redisClientMultiCommandPrototype?.addCommand)) { this._unwrap(redisClientMultiCommandPrototype, 'addCommand'); } }, ); const clientIndexModule = new instrumentation.InstrumentationNodeModuleFile( `${basePackageName}/dist/lib/client/index.js`, ['^1.0.0', '>=5.0.0 <5.12.0'], (moduleExports) => { const redisClientPrototype = moduleExports?.default?.prototype; if (redisClientPrototype?.multi) { if (instrumentation.isWrapped(redisClientPrototype?.multi)) { this._unwrap(redisClientPrototype, 'multi'); } this._wrap(redisClientPrototype, 'multi', this._getPatchRedisClientMulti()); } if (redisClientPrototype?.MULTI) { if (instrumentation.isWrapped(redisClientPrototype?.MULTI)) { this._unwrap(redisClientPrototype, 'MULTI'); } this._wrap(redisClientPrototype, 'MULTI', this._getPatchRedisClientMulti()); } if (instrumentation.isWrapped(redisClientPrototype?.sendCommand)) { this._unwrap(redisClientPrototype, 'sendCommand'); } this._wrap(redisClientPrototype, 'sendCommand', this._getPatchRedisClientSendCommand()); if (instrumentation.isWrapped(redisClientPrototype?.connect)) { this._unwrap(redisClientPrototype, 'connect'); } this._wrap(redisClientPrototype, 'connect', this._getPatchedClientConnect()); return moduleExports; }, (moduleExports) => { const redisClientPrototype = moduleExports?.default?.prototype; if (instrumentation.isWrapped(redisClientPrototype?.multi)) { this._unwrap(redisClientPrototype, 'multi'); } if (instrumentation.isWrapped(redisClientPrototype?.MULTI)) { this._unwrap(redisClientPrototype, 'MULTI'); } if (instrumentation.isWrapped(redisClientPrototype?.sendCommand)) { this._unwrap(redisClientPrototype, 'sendCommand'); } if (instrumentation.isWrapped(redisClientPrototype?.connect)) { this._unwrap(redisClientPrototype, 'connect'); } }, ); return new instrumentation.InstrumentationNodeModuleDefinition( basePackageName, ['^1.0.0', '>=5.0.0 <5.12.0'], (moduleExports) => moduleExports, () => {}, [commanderModuleFile, multiCommanderModule, clientIndexModule], ); } _getPatchExtendWithCommands(transformCommandArguments) { const plugin = this; return function extendWithCommandsPatchWrapper(original) { return function extendWithCommandsPatch( config) { if (config?.BaseClass?.name !== 'RedisClient') { return original.apply(this, arguments); } const origExecutor = config.executor; config.executor = function ( command, args) { const redisCommandArguments = transformCommandArguments(command, args).args; return plugin._traceClientCommand(origExecutor, this, arguments, redisCommandArguments); }; return original.apply(this, arguments); }; }; } _getPatchMultiCommandsExec(isPipeline) { const plugin = this; return function execPatchWrapper(original) { return function execPatch() { const execRes = original.apply(this, arguments); if (typeof execRes?.then !== 'function') { plugin._diag.error('non-promise result when patching exec/execAsPipeline'); return execRes; } return execRes .then((redisRes) => { const openSpans = this[OTEL_OPEN_SPANS]; plugin._endSpansWithRedisReplies(openSpans, redisRes, isPipeline); return redisRes; }) .catch((err) => { const openSpans = this[OTEL_OPEN_SPANS]; if (!openSpans) { plugin._diag.error('cannot find open spans to end for multi/pipeline'); } else { const replies = err.constructor.name === 'MultiErrorReply' ? (err ).replies : new Array(openSpans.length).fill(err); plugin._endSpansWithRedisReplies(openSpans, replies, isPipeline); } return Promise.reject(err); }); }; }; } _getPatchMultiCommandsAddCommand() { const plugin = this; return function addCommandWrapper(original) { return function addCommandPatch( args) { return plugin._traceClientCommand(original, this, arguments, args); }; }; } _getPatchRedisClientMulti() { return function multiPatchWrapper(original) { return function multiPatch() { const multiRes = original.apply(this, arguments); multiRes[MULTI_COMMAND_OPTIONS] = this.options; return multiRes; }; }; } _getPatchRedisClientSendCommand() { const plugin = this; return function sendCommandWrapper(original) { return function sendCommandPatch( args) { return plugin._traceClientCommand(original, this, arguments, args); }; }; } _getPatchedClientConnect() { const plugin = this; return function connectWrapper(original) { return function patchedConnect() { const options = this.options; const attributes = getClientAttributes(plugin._diag, options, plugin._semconvStability); attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.db.otel.redis'; const span = plugin.tracer.startSpan(`${RedisInstrumentationV4_V5.COMPONENT}-connect`, { kind: api.SpanKind.CLIENT, attributes, }); const res = api.context.with(api.trace.setSpan(api.context.active(), span), () => { return original.apply(this); }); return res .then((result) => { span.end(); return result; }) .catch((error) => { span.recordException(error); span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message, }); span.end(); return Promise.reject(error); }); }; }; } _traceClientCommand( origFunction, origThis, origArguments, redisCommandArguments, ) { const hasNoParentSpan = api.trace.getSpan(api.context.active()) === undefined; if (hasNoParentSpan && this.getConfig().requireParentSpan) { return origFunction.apply(origThis, origArguments); } const clientOptions = origThis.options || origThis[MULTI_COMMAND_OPTIONS]; const commandName = redisCommandArguments[0] ; const commandArgs = redisCommandArguments.slice(1); const dbStatementSerializer = this.getConfig().dbStatementSerializer || redisCommon.defaultDbStatementSerializer; const attributes = getClientAttributes(this._diag, clientOptions, this._semconvStability); if (this._semconvStability & instrumentation.SemconvStability.STABLE) { attributes[semanticConventions.ATTR_DB_OPERATION_NAME] = commandName; } try { const dbStatement = dbStatementSerializer(commandName, commandArgs); if (dbStatement != null) { if (this._semconvStability & instrumentation.SemconvStability.OLD) { attributes[semconv.ATTR_DB_STATEMENT] = dbStatement; } if (this._semconvStability & instrumentation.SemconvStability.STABLE) { attributes[semanticConventions.ATTR_DB_QUERY_TEXT] = dbStatement; } } } catch (e) { this._diag.error('dbStatementSerializer throw an exception', e, { commandName }); } attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.db.otel.redis'; const span = this.tracer.startSpan(`${RedisInstrumentationV4_V5.COMPONENT}-${commandName}`, { kind: api.SpanKind.CLIENT, attributes, }); const res = api.context.with(api.trace.setSpan(api.context.active(), span), () => { return origFunction.apply(origThis, origArguments); }); if (typeof res?.then === 'function') { res.then( (redisRes) => { this._endSpanWithResponse(span, commandName, commandArgs, redisRes, undefined); }, (err) => { this._endSpanWithResponse(span, commandName, commandArgs, null, err); }, ); } else { const redisClientMultiCommand = res; redisClientMultiCommand[OTEL_OPEN_SPANS] = redisClientMultiCommand[OTEL_OPEN_SPANS] || []; redisClientMultiCommand[OTEL_OPEN_SPANS].push({ span, commandName, commandArgs, }); } return res; } _endSpansWithRedisReplies(openSpans, replies, isPipeline = false) { if (!openSpans) { return this._diag.error('cannot find open spans to end for redis multi/pipeline'); } if (replies.length !== openSpans.length) { return this._diag.error('number of multi command spans does not match response from redis'); } const allCommands = openSpans.map(s => s.commandName); const allSameCommand = allCommands.every(cmd => cmd === allCommands[0]); const operationName = allSameCommand ? (isPipeline ? 'PIPELINE ' : 'MULTI ') + allCommands[0] : isPipeline ? 'PIPELINE' : 'MULTI'; for (let i = 0; i < openSpans.length; i++) { const { span, commandArgs } = openSpans[i]; const currCommandRes = replies[i]; const [res, err] = currCommandRes instanceof Error ? [null, currCommandRes] : [currCommandRes, undefined]; if (this._semconvStability & instrumentation.SemconvStability.STABLE) { span.setAttribute(semanticConventions.ATTR_DB_OPERATION_NAME, operationName); } this._endSpanWithResponse(span, allCommands[i], commandArgs, res, err); } } _endSpanWithResponse( span, commandName, commandArgs, response, error, ) { const { responseHook } = this.getConfig(); if (!error && responseHook) { try { responseHook(span, commandName, commandArgs, response); } catch (err) { this._diag.error('responseHook throw an exception', err); } } if (error) { span.recordException(error); span.setStatus({ code: api.SpanStatusCode.ERROR, message: error?.message }); } span.end(); } } RedisInstrumentationV4_V5.__initStatic2(); // ---- RedisInstrumentation (wrapper) ---- const DEFAULT_CONFIG = { requireParentSpan: false, }; class RedisInstrumentation extends instrumentation.InstrumentationBase { __init() {this.initialized = false;} constructor(config = {}) { const resolvedConfig = { ...DEFAULT_CONFIG, ...config }; super(PACKAGE_NAME, PACKAGE_VERSION, resolvedConfig);RedisInstrumentation.prototype.__init.call(this); this.instrumentationV2_V3 = new RedisInstrumentationV2_V3(this.getConfig()); this.instrumentationV4_V5 = new RedisInstrumentationV4_V5(this.getConfig()); this.initialized = true; } setConfig(config = {}) { const newConfig = { ...DEFAULT_CONFIG, ...config }; super.setConfig(newConfig); if (!this.initialized) { return; } this.instrumentationV2_V3.setConfig(newConfig); this.instrumentationV4_V5.setConfig(newConfig); } init() {} getModuleDefinitions() { return [...this.instrumentationV2_V3.getModuleDefinitions(), ...this.instrumentationV4_V5.getModuleDefinitions()]; } setTracerProvider(tracerProvider) { super.setTracerProvider(tracerProvider); if (!this.initialized) { return; } this.instrumentationV2_V3.setTracerProvider(tracerProvider); this.instrumentationV4_V5.setTracerProvider(tracerProvider); } enable() { super.enable(); if (!this.initialized) { return; } this.instrumentationV2_V3.enable(); this.instrumentationV4_V5.enable(); } disable() { super.disable(); if (!this.initialized) { return; } this.instrumentationV2_V3.disable(); this.instrumentationV4_V5.disable(); } } exports.RedisInstrumentation = RedisInstrumentation; //# sourceMappingURL=redis-instrumentation.js.map