UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

451 lines (448 loc) 17.7 kB
import { diag, context, trace, SpanKind, propagation, SpanStatusCode, ROOT_CONTEXT } from '@opentelemetry/api'; import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; import { InstrumentationBase, semconvStabilityFromStr, InstrumentationNodeModuleFile, InstrumentationNodeModuleDefinition, isWrapped, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; import { ATTR_MESSAGING_OPERATION } from './semconv.js'; import { MESSAGING_DESTINATION_KIND_VALUE_TOPIC, ATTR_MESSAGING_CONVERSATION_ID, OLD_ATTR_MESSAGING_MESSAGE_ID, ATTR_MESSAGING_RABBITMQ_ROUTING_KEY, ATTR_MESSAGING_DESTINATION_KIND, ATTR_MESSAGING_DESTINATION, MESSAGING_OPERATION_VALUE_PROCESS } from './semconv-obsolete.js'; import { DEFAULT_CONFIG, EndOperation } from './types.js'; import { getConnectionAttributesFromUrl, getConnectionAttributesFromServer, CONNECTION_ATTRIBUTES, CHANNEL_CONSUME_TIMEOUT_TIMER, CHANNEL_SPANS_NOT_ENDED, markConfirmChannelTracing, unmarkConfirmChannelTracing, isConfirmChannelTracing, MESSAGE_STORED_SPAN, normalizeExchange } from './utils.js'; import { SDK_VERSION } from '@sentry/core'; const PACKAGE_NAME = "@sentry/instrumentation-amqplib"; const supportedVersions = [">=0.5.5 <2"]; class AmqplibInstrumentation extends InstrumentationBase { constructor(config = {}) { super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); this._setSemconvStabilityFromEnv(); } // Used for testing. _setSemconvStabilityFromEnv() { this._netSemconvStability = semconvStabilityFromStr("http", process.env.OTEL_SEMCONV_STABILITY_OPT_IN); } setConfig(config = {}) { super.setConfig({ ...DEFAULT_CONFIG, ...config }); } init() { const channelModelModuleFile = new InstrumentationNodeModuleFile( "amqplib/lib/channel_model.js", supportedVersions, this.patchChannelModel.bind(this), this.unpatchChannelModel.bind(this) ); const callbackModelModuleFile = new InstrumentationNodeModuleFile( "amqplib/lib/callback_model.js", supportedVersions, this.patchChannelModel.bind(this), this.unpatchChannelModel.bind(this) ); const connectModuleFile = new InstrumentationNodeModuleFile( "amqplib/lib/connect.js", supportedVersions, this.patchConnect.bind(this), this.unpatchConnect.bind(this) ); const module = new InstrumentationNodeModuleDefinition("amqplib", supportedVersions, void 0, void 0, [ channelModelModuleFile, connectModuleFile, callbackModelModuleFile ]); return module; } patchConnect(moduleExports) { moduleExports = this.unpatchConnect(moduleExports); if (!isWrapped(moduleExports.connect)) { this._wrap(moduleExports, "connect", this.getConnectPatch.bind(this)); } return moduleExports; } unpatchConnect(moduleExports) { if (isWrapped(moduleExports.connect)) { this._unwrap(moduleExports, "connect"); } return moduleExports; } patchChannelModel(moduleExports, moduleVersion) { if (!isWrapped(moduleExports.Channel.prototype.publish)) { this._wrap(moduleExports.Channel.prototype, "publish", this.getPublishPatch.bind(this, moduleVersion)); } if (!isWrapped(moduleExports.Channel.prototype.consume)) { this._wrap(moduleExports.Channel.prototype, "consume", this.getConsumePatch.bind(this, moduleVersion)); } if (!isWrapped(moduleExports.Channel.prototype.ack)) { this._wrap(moduleExports.Channel.prototype, "ack", this.getAckPatch.bind(this, false, EndOperation.Ack)); } if (!isWrapped(moduleExports.Channel.prototype.nack)) { this._wrap(moduleExports.Channel.prototype, "nack", this.getAckPatch.bind(this, true, EndOperation.Nack)); } if (!isWrapped(moduleExports.Channel.prototype.reject)) { this._wrap(moduleExports.Channel.prototype, "reject", this.getAckPatch.bind(this, true, EndOperation.Reject)); } if (!isWrapped(moduleExports.Channel.prototype.ackAll)) { this._wrap(moduleExports.Channel.prototype, "ackAll", this.getAckAllPatch.bind(this, false, EndOperation.AckAll)); } if (!isWrapped(moduleExports.Channel.prototype.nackAll)) { this._wrap( moduleExports.Channel.prototype, "nackAll", this.getAckAllPatch.bind(this, true, EndOperation.NackAll) ); } if (!isWrapped(moduleExports.Channel.prototype.emit)) { this._wrap(moduleExports.Channel.prototype, "emit", this.getChannelEmitPatch.bind(this)); } if (!isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { this._wrap( moduleExports.ConfirmChannel.prototype, "publish", this.getConfirmedPublishPatch.bind(this, moduleVersion) ); } return moduleExports; } unpatchChannelModel(moduleExports) { if (isWrapped(moduleExports.Channel.prototype.publish)) { this._unwrap(moduleExports.Channel.prototype, "publish"); } if (isWrapped(moduleExports.Channel.prototype.consume)) { this._unwrap(moduleExports.Channel.prototype, "consume"); } if (isWrapped(moduleExports.Channel.prototype.ack)) { this._unwrap(moduleExports.Channel.prototype, "ack"); } if (isWrapped(moduleExports.Channel.prototype.nack)) { this._unwrap(moduleExports.Channel.prototype, "nack"); } if (isWrapped(moduleExports.Channel.prototype.reject)) { this._unwrap(moduleExports.Channel.prototype, "reject"); } if (isWrapped(moduleExports.Channel.prototype.ackAll)) { this._unwrap(moduleExports.Channel.prototype, "ackAll"); } if (isWrapped(moduleExports.Channel.prototype.nackAll)) { this._unwrap(moduleExports.Channel.prototype, "nackAll"); } if (isWrapped(moduleExports.Channel.prototype.emit)) { this._unwrap(moduleExports.Channel.prototype, "emit"); } if (isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { this._unwrap(moduleExports.ConfirmChannel.prototype, "publish"); } return moduleExports; } getConnectPatch(original) { const self = this; return function patchedConnect(url, socketOptions, openCallback) { return original.call( this, url, socketOptions, function(err, conn) { if (err == null) { const urlAttributes = getConnectionAttributesFromUrl(url, self._netSemconvStability); const serverAttributes = getConnectionAttributesFromServer(conn); conn[CONNECTION_ATTRIBUTES] = { ...urlAttributes, ...serverAttributes }; } openCallback.apply(this, arguments); } ); }; } getChannelEmitPatch(original) { const self = this; return function emit(eventName) { if (eventName === "close") { self.endAllSpansOnChannel(this, true, EndOperation.ChannelClosed, void 0); const activeTimer = this[CHANNEL_CONSUME_TIMEOUT_TIMER]; if (activeTimer) { clearInterval(activeTimer); } this[CHANNEL_CONSUME_TIMEOUT_TIMER] = void 0; } else if (eventName === "error") { self.endAllSpansOnChannel(this, true, EndOperation.ChannelError, void 0); } return original.apply(this, arguments); }; } getAckAllPatch(isRejected, endOperation, original) { const self = this; return function ackAll(requeueOrEmpty) { self.endAllSpansOnChannel(this, isRejected, endOperation, requeueOrEmpty); return original.apply(this, arguments); }; } getAckPatch(isRejected, endOperation, original) { const self = this; return function ack(message, allUpToOrRequeue, requeue) { const channel = this; const requeueResolved = endOperation === EndOperation.Reject ? allUpToOrRequeue : requeue; const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; const msgIndex = spansNotEnded.findIndex((msgDetails) => msgDetails.msg === message); if (msgIndex < 0) { self.endConsumerSpan(message, isRejected, endOperation, requeueResolved); } else if (endOperation !== EndOperation.Reject && allUpToOrRequeue) { for (let i = 0; i <= msgIndex; i++) { self.endConsumerSpan(spansNotEnded[i].msg, isRejected, endOperation, requeueResolved); } spansNotEnded.splice(0, msgIndex + 1); } else { self.endConsumerSpan(message, isRejected, endOperation, requeueResolved); spansNotEnded.splice(msgIndex, 1); } return original.apply(this, arguments); }; } getConsumePatch(moduleVersion, original) { const self = this; return function consume(queue, onMessage, options) { const channel = this; if (!Object.prototype.hasOwnProperty.call(channel, CHANNEL_SPANS_NOT_ENDED)) { const { consumeTimeoutMs } = self.getConfig(); if (consumeTimeoutMs) { const timer = setInterval(() => { self.checkConsumeTimeoutOnChannel(channel); }, consumeTimeoutMs); timer.unref(); channel[CHANNEL_CONSUME_TIMEOUT_TIMER] = timer; } channel[CHANNEL_SPANS_NOT_ENDED] = []; } const patchedOnMessage = function(msg) { if (!msg) { return onMessage.call(this, msg); } const headers = msg.properties.headers ?? {}; let parentContext = propagation.extract(ROOT_CONTEXT, headers); const exchange = msg.fields?.exchange; let links; if (self._config.useLinksForConsume) { const parentSpanContext = parentContext ? trace.getSpan(parentContext)?.spanContext() : void 0; parentContext = void 0; if (parentSpanContext) { links = [ { context: parentSpanContext } ]; } } const span = self.tracer.startSpan( `${queue} process`, { kind: SpanKind.CONSUMER, attributes: { ...channel?.connection?.[CONNECTION_ATTRIBUTES], [ATTR_MESSAGING_DESTINATION]: exchange, [ATTR_MESSAGING_DESTINATION_KIND]: MESSAGING_DESTINATION_KIND_VALUE_TOPIC, [ATTR_MESSAGING_RABBITMQ_ROUTING_KEY]: msg.fields?.routingKey, [ATTR_MESSAGING_OPERATION]: MESSAGING_OPERATION_VALUE_PROCESS, [OLD_ATTR_MESSAGING_MESSAGE_ID]: msg?.properties.messageId, [ATTR_MESSAGING_CONVERSATION_ID]: msg?.properties.correlationId }, links }, parentContext ); const { consumeHook } = self.getConfig(); if (consumeHook) { safeExecuteInTheMiddle( () => consumeHook(span, { moduleVersion, msg }), (e) => { if (e) { diag.error("amqplib instrumentation: consumerHook error", e); } }, true ); } if (!options?.noAck) { channel[CHANNEL_SPANS_NOT_ENDED].push({ msg, timeOfConsume: hrTime() }); msg[MESSAGE_STORED_SPAN] = span; } const setContext = parentContext ? parentContext : ROOT_CONTEXT; context.with(trace.setSpan(setContext, span), () => { onMessage.call(this, msg); }); if (options?.noAck) { self.callConsumeEndHook(span, msg, false, EndOperation.AutoAck); span.end(); } }; arguments[1] = patchedOnMessage; return original.apply(this, arguments); }; } getConfirmedPublishPatch(moduleVersion, original) { const self = this; return function confirmedPublish(exchange, routingKey, content, options, callback) { const channel = this; const { span, modifiedOptions } = self.createPublishSpan(self, exchange, routingKey, channel, options); const { publishHook } = self.getConfig(); if (publishHook) { safeExecuteInTheMiddle( () => publishHook(span, { moduleVersion, exchange, routingKey, content, options: modifiedOptions, isConfirmChannel: true }), (e) => { if (e) { diag.error("amqplib instrumentation: publishHook error", e); } }, true ); } const patchedOnConfirm = function(err, ok) { try { callback?.call(this, err, ok); } finally { const { publishConfirmHook } = self.getConfig(); if (publishConfirmHook) { safeExecuteInTheMiddle( () => publishConfirmHook(span, { moduleVersion, exchange, routingKey, content, options, isConfirmChannel: true, confirmError: err }), (e) => { if (e) { diag.error("amqplib instrumentation: publishConfirmHook error", e); } }, true ); } if (err) { span.setStatus({ code: SpanStatusCode.ERROR, message: "message confirmation has been nack'ed" }); } span.end(); } }; const markedContext = markConfirmChannelTracing(context.active()); const argumentsCopy = [...arguments]; argumentsCopy[3] = modifiedOptions; argumentsCopy[4] = context.bind( unmarkConfirmChannelTracing(trace.setSpan(markedContext, span)), patchedOnConfirm ); return context.with(markedContext, original.bind(this, ...argumentsCopy)); }; } getPublishPatch(moduleVersion, original) { const self = this; return function publish(exchange, routingKey, content, options) { if (isConfirmChannelTracing(context.active())) { return original.apply(this, arguments); } else { const channel = this; const { span, modifiedOptions } = self.createPublishSpan(self, exchange, routingKey, channel, options); const { publishHook } = self.getConfig(); if (publishHook) { safeExecuteInTheMiddle( () => publishHook(span, { moduleVersion, exchange, routingKey, content, options: modifiedOptions, isConfirmChannel: false }), (e) => { if (e) { diag.error("amqplib instrumentation: publishHook error", e); } }, true ); } const argumentsCopy = [...arguments]; argumentsCopy[3] = modifiedOptions; const originalRes = original.apply(this, argumentsCopy); span.end(); return originalRes; } }; } createPublishSpan(self, exchange, routingKey, channel, options) { const normalizedExchange = normalizeExchange(exchange); const span = self.tracer.startSpan(`publish ${normalizedExchange}`, { kind: SpanKind.PRODUCER, attributes: { ...channel.connection[CONNECTION_ATTRIBUTES], [ATTR_MESSAGING_DESTINATION]: exchange, [ATTR_MESSAGING_DESTINATION_KIND]: MESSAGING_DESTINATION_KIND_VALUE_TOPIC, [ATTR_MESSAGING_RABBITMQ_ROUTING_KEY]: routingKey, [OLD_ATTR_MESSAGING_MESSAGE_ID]: options?.messageId, [ATTR_MESSAGING_CONVERSATION_ID]: options?.correlationId } }); const modifiedOptions = options ?? {}; modifiedOptions.headers = modifiedOptions.headers ?? {}; propagation.inject(trace.setSpan(context.active(), span), modifiedOptions.headers); return { span, modifiedOptions }; } endConsumerSpan(message, isRejected, operation, requeue) { const storedSpan = message[MESSAGE_STORED_SPAN]; if (!storedSpan) return; if (isRejected !== false) { storedSpan.setStatus({ code: SpanStatusCode.ERROR, message: operation !== EndOperation.ChannelClosed && operation !== EndOperation.ChannelError ? `${operation} called on message${requeue === true ? " with requeue" : requeue === false ? " without requeue" : ""}` : operation }); } this.callConsumeEndHook(storedSpan, message, isRejected, operation); storedSpan.end(); message[MESSAGE_STORED_SPAN] = void 0; } endAllSpansOnChannel(channel, isRejected, operation, requeue) { const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; spansNotEnded.forEach((msgDetails) => { this.endConsumerSpan(msgDetails.msg, isRejected, operation, requeue); }); channel[CHANNEL_SPANS_NOT_ENDED] = []; } callConsumeEndHook(span, msg, rejected, endOperation) { const { consumeEndHook } = this.getConfig(); if (!consumeEndHook) return; safeExecuteInTheMiddle( () => consumeEndHook(span, { msg, rejected, endOperation }), (e) => { if (e) { diag.error("amqplib instrumentation: consumerEndHook error", e); } }, true ); } checkConsumeTimeoutOnChannel(channel) { const currentTime = hrTime(); const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; let i; const { consumeTimeoutMs } = this.getConfig(); for (i = 0; i < spansNotEnded.length; i++) { const currMessage = spansNotEnded[i]; const timeFromConsume = hrTimeDuration(currMessage.timeOfConsume, currentTime); if (hrTimeToMilliseconds(timeFromConsume) < consumeTimeoutMs) { break; } this.endConsumerSpan(currMessage.msg, null, EndOperation.InstrumentationTimeout, true); } spansNotEnded.splice(0, i); } } export { AmqplibInstrumentation }; //# sourceMappingURL=amqplib.js.map