From 8ce5b2e6b5ee6cc718f2abbde7353bf8edb66855 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 13:57:29 +0200 Subject: [PATCH 01/39] feat: amqplib instrumentation --- .github/workflows/unit-test.yml | 7 + .husky/commit-msg | 4 - .../node/instrumentation-amqplib/package.json | 78 ++ .../instrumentation-amqplib/src/amqplib.ts | 745 ++++++++++++++ .../node/instrumentation-amqplib/src/index.ts | 2 + .../node/instrumentation-amqplib/src/types.ts | 79 ++ .../node/instrumentation-amqplib/src/utils.ts | 207 ++++ .../test/amqplib-callbacks.test.ts | 237 +++++ .../test/amqplib-connection.test.ts | 105 ++ .../test/amqplib-promise.test.ts | 936 ++++++++++++++++++ .../instrumentation-amqplib/test/config.ts | 7 + .../test/utils.test.ts | 152 +++ .../instrumentation-amqplib/test/utils.ts | 73 ++ .../instrumentation-amqplib/tsconfig.json | 11 + 14 files changed, 2639 insertions(+), 4 deletions(-) delete mode 100755 .husky/commit-msg create mode 100644 plugins/node/instrumentation-amqplib/package.json create mode 100644 plugins/node/instrumentation-amqplib/src/amqplib.ts create mode 100644 plugins/node/instrumentation-amqplib/src/index.ts create mode 100644 plugins/node/instrumentation-amqplib/src/types.ts create mode 100644 plugins/node/instrumentation-amqplib/src/utils.ts create mode 100644 plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts create mode 100644 plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts create mode 100644 plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts create mode 100644 plugins/node/instrumentation-amqplib/test/config.ts create mode 100644 plugins/node/instrumentation-amqplib/test/utils.test.ts create mode 100644 plugins/node/instrumentation-amqplib/test/utils.ts create mode 100644 plugins/node/instrumentation-amqplib/tsconfig.json diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 95b8d6e82b..c266704367 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -81,6 +81,13 @@ jobs: image: bitnami/cassandra:3 ports: - 9042:9042 + rabbitmq: + image: rabbitmq:3 + ports: + - 22221:5672 + env: + RABBITMQ_DEFAULT_USER: username + RABBITMQ_DEFAULT_PASS: password env: RUN_CASSANDRA_TESTS: 1 RUN_MEMCACHED_TESTS: 1 diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index e8511eaeaf..0000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install commitlint --edit $1 diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json new file mode 100644 index 0000000000..9a6568b75f --- /dev/null +++ b/plugins/node/instrumentation-amqplib/package.json @@ -0,0 +1,78 @@ +{ + "name": "@opentelemetry/instrumentation-amqplib", + "version": "0.27.0", + "description": "OpenTelemetry automatic instrumentation for the `amqplib` package", + "keywords": [ + "amqplib", + "opentelemetry", + "rabbitmq", + "AMQP 0-9-1" + ], + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib#readme", + "license": "Apache-2.0", + "author": "OpenTelemetry Authors", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues" + }, + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "clean": "rimraf build/*", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "npm run version:update && tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version && lerna run version --scope $(npm pkg get name) --include-dependencies", + "prewatch": "npm run precompile", + "prepare": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.test.ts'", + "test-all-versions": "tav", + "version:update": "node ../../../scripts/version-update.js", + "watch": "tsc -w", + "test:docker:run": "docker run -d --hostname demo-amqplib-rabbit --name amqplib-unittests -p 22221:5672 --env RABBITMQ_DEFAULT_USER=username --env RABBITMQ_DEFAULT_PASS=password rabbitmq:3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.1" + }, + "dependencies": { + "@opentelemetry/core": "^1.0.0", + "@opentelemetry/instrumentation": "^0.27.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/amqplib": "^0.5.17" + }, + "devDependencies": { + "@opentelemetry/api": "1.0.1", + "@types/lodash": "4.14.178", + "@types/mocha": "8.2.3", + "@types/sinon": "10.0.2", + "@types/node": "16.11.21", + "amqplib": "0.8.0", + "expect": "27.4.2", + "lodash": "4.17.21", + "mocha": "7.2.0", + "ts-mocha": "8.0.0", + "nyc": "15.1.0", + "gts": "3.1.0", + "codecov": "3.8.3", + "@opentelemetry/contrib-test-utils": "^0.28.0", + "sinon": "13.0.1", + "test-all-versions": "5.0.1", + "typescript": "4.3.5" + }, + "engines": { + "node": ">=8.5.0" + } +} \ No newline at end of file diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts new file mode 100644 index 0000000000..378707f415 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -0,0 +1,745 @@ +import { + context, + diag, + propagation, + trace, + Span, + SpanKind, + SpanStatusCode, +} from "@opentelemetry/api"; +import { + InstrumentationBase, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + safeExecuteInTheMiddle, +} from "@opentelemetry/instrumentation"; +import { + SemanticAttributes, + MessagingOperationValues, + MessagingDestinationKindValues, +} from "@opentelemetry/semantic-conventions"; +import type * as amqp from "amqplib"; +import { + AmqplibInstrumentationConfig, + DEFAULT_CONFIG, + EndOperation, +} from "./types"; +import { + CHANNEL_CONSUME_TIMEOUT_TIMER, + CHANNEL_SPANS_NOT_ENDED, + CONNECTION_ATTRIBUTES, + getConnectionAttributesFromServer, + getConnectionAttributesFromUrl, + InstrumentationConsumeChannel, + InstrumentationMessage, + InstrumentationPublishChannel, + isConfirmChannelTracing, + markConfirmChannelTracing, + MESSAGE_STORED_SPAN, + normalizeExchange, + unmarkConfirmChannelTracing, +} from "./utils"; +import { VERSION } from "./version"; + +export class AmqplibInstrumentation extends InstrumentationBase { + protected override _config!: AmqplibInstrumentationConfig; + + constructor(config: AmqplibInstrumentationConfig = {}) { + super( + "@opentelemetry/instrumentation-amqplib", + VERSION, + Object.assign({}, DEFAULT_CONFIG, config) + ); + } + + override setConfig(config: AmqplibInstrumentationConfig = {}) { + this._config = Object.assign({}, DEFAULT_CONFIG, config); + } + + protected init(): InstrumentationModuleDefinition { + const channelModelModuleFile = + new InstrumentationNodeModuleFile( + `amqplib/lib/channel_model.js`, + [">=0.5.5"], + this.patchChannelModel.bind(this), + this.unpatchChannelModel.bind(this) + ); + + const callbackModelModuleFile = + new InstrumentationNodeModuleFile( + `amqplib/lib/callback_model.js`, + [">=0.5.5"], + this.patchChannelModel.bind(this), + this.unpatchChannelModel.bind(this) + ); + + const connectModuleFile = new InstrumentationNodeModuleFile( + `amqplib/lib/connect.js`, + [">=0.5.5"], + this.patchConnect.bind(this), + this.unpatchConnect.bind(this) + ); + + const module = new InstrumentationNodeModuleDefinition( + "amqplib", + [">=0.5.5"], + undefined, + undefined, + [channelModelModuleFile, connectModuleFile, callbackModelModuleFile] + ); + return module; + } + + private patchConnect(moduleExports: any) { + moduleExports = this.unpatchConnect(moduleExports); + if (!isWrapped(moduleExports.connect)) { + this._wrap(moduleExports, "connect", this.getConnectPatch.bind(this)); + } + return moduleExports; + } + + private unpatchConnect(moduleExports: any) { + if (isWrapped(moduleExports.connect)) { + this._unwrap(moduleExports, "connect"); + } + return moduleExports; + } + + private patchChannelModel( + moduleExports: any, + moduleVersion: string | undefined + ) { + 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; + } + + private unpatchChannelModel(moduleExports: any) { + 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; + } + + private getConnectPatch( + original: ( + url: string | amqp.Options.Connect, + socketOptions: any, + openCallback: (err: any, connection: amqp.Connection) => void + ) => amqp.Connection + ) { + return function patchedConnect( + this: unknown, + url: string | amqp.Options.Connect, + socketOptions: any, + openCallback: Function + ) { + return original.call( + this, + url, + socketOptions, + function (this: unknown, err, conn: amqp.Connection) { + if (err === null) { + const urlAttributes = getConnectionAttributesFromUrl(url); + // the type of conn in @types/amqplib is amqp.Connection, but in practice the library send the + // `serverProperties` on the `conn` and not in a property `connection`. + // I don't have capacity to debug it currently but it should probably be fixed in @types or + // in the package itself + // currently setting as any to calm typescript + const serverAttributes = getConnectionAttributesFromServer( + conn as any + ); + Object.defineProperty(conn, CONNECTION_ATTRIBUTES, { + value: { ...urlAttributes, ...serverAttributes }, + enumerable: false, + }); + } + openCallback.apply(this, arguments); + } + ); + }; + } + + private getChannelEmitPatch(original: Function) { + const self = this; + return function emit( + this: InstrumentationConsumeChannel, + eventName: string + ) { + if (eventName === "close") { + self.endAllSpansOnChannel( + this, + true, + EndOperation.ChannelClosed, + undefined + ); + const activeTimer = this[CHANNEL_CONSUME_TIMEOUT_TIMER]; + if (activeTimer) { + clearInterval(activeTimer); + } + delete this[CHANNEL_CONSUME_TIMEOUT_TIMER]; + } else if (eventName === "error") { + self.endAllSpansOnChannel( + this, + true, + EndOperation.ChannelError, + undefined + ); + } + return original.apply(this, arguments); + }; + } + + private getAckAllPatch( + isRejected: boolean, + endOperation: EndOperation, + original: Function + ) { + const self = this; + return function ackAll( + this: InstrumentationConsumeChannel, + requeueOrEmpty?: boolean + ): void { + self.endAllSpansOnChannel(this, isRejected, endOperation, requeueOrEmpty); + return original.apply(this, arguments); + }; + } + + private getAckPatch( + isRejected: boolean, + endOperation: EndOperation, + original: Function + ) { + const self = this; + return function ack( + this: InstrumentationConsumeChannel, + message: amqp.Message, + allUpToOrRequeue?: boolean, + requeue?: boolean + ): void { + const channel = this; + // we use this patch in reject function as well, but it has different signature + const requeueResolved = + endOperation === EndOperation.Reject ? allUpToOrRequeue : requeue; + + const spansNotEnded: { msg: amqp.Message }[] = + channel[CHANNEL_SPANS_NOT_ENDED] ?? []; + const msgIndex = spansNotEnded.findIndex( + (msgDetails) => msgDetails.msg === message + ); + if (msgIndex < 0) { + // should not happen in happy flow + // but possible if user is calling the api function ack twice with same message + 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); + }; + } + + protected getConsumePatch( + moduleVersion: string | undefined, + original: Function + ) { + const self = this; + return function consume( + this: InstrumentationConsumeChannel, + queue: string, + onMessage: (msg: amqp.ConsumeMessage | null) => void, + options?: amqp.Options.Consume + ): Promise { + const channel = this; + if (!channel.hasOwnProperty(CHANNEL_SPANS_NOT_ENDED)) { + if (self._config.consumeTimeoutMs) { + const timer = setInterval(() => { + self.checkConsumeTimeoutOnChannel(channel); + }, self._config.consumeTimeoutMs); + timer.unref(); + Object.defineProperty(channel, CHANNEL_CONSUME_TIMEOUT_TIMER, { + value: timer, + enumerable: false, + configurable: true, + }); + } + Object.defineProperty(channel, CHANNEL_SPANS_NOT_ENDED, { + value: [], + enumerable: false, + configurable: true, + }); + } + + const patchedOnMessage = function ( + this: unknown, + msg: amqp.ConsumeMessage | null + ) { + // msg is expected to be null for signaling consumer cancel notification + // https://www.rabbitmq.com/consumer-cancel.html + // in this case, we do not start a span, as this is not a real message. + if (!msg) { + return onMessage.call(this, msg); + } + + const headers = msg.properties.headers ?? {}; + const parentContext = propagation.extract(context.active(), headers); + const exchange = msg.fields?.exchange; + const span = self.tracer.startSpan( + `${queue} process`, + { + kind: SpanKind.CONSUMER, + attributes: { + ...channel?.connection?.[CONNECTION_ATTRIBUTES], + [SemanticAttributes.MESSAGING_DESTINATION]: exchange, + [SemanticAttributes.MESSAGING_DESTINATION_KIND]: + MessagingDestinationKindValues.TOPIC, + [SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]: + msg.fields?.routingKey, + [SemanticAttributes.MESSAGING_OPERATION]: + MessagingOperationValues.PROCESS, + [SemanticAttributes.MESSAGING_MESSAGE_ID]: + msg?.properties.messageId, + [SemanticAttributes.MESSAGING_CONVERSATION_ID]: + msg?.properties.correlationId, + }, + }, + parentContext + ); + + if (self._config.moduleVersionAttributeName && moduleVersion) { + span.setAttribute( + self._config.moduleVersionAttributeName, + moduleVersion + ); + } + + if (self._config.consumeHook) { + safeExecuteInTheMiddle( + () => self._config.consumeHook!(span, msg), + (e) => { + if (e) { + diag.error("amqplib instrumentation: consumerHook error", e); + } + }, + true + ); + } + + if (!options?.noAck) { + // store the message on the channel so we can close the span on ackAll etc + channel[CHANNEL_SPANS_NOT_ENDED]!.push({ + msg, + timeOfConsume: new Date(), + }); + + // store the span on the message, so we can end it when user call 'ack' on it + Object.defineProperty(msg, MESSAGE_STORED_SPAN, { + value: span, + enumerable: false, + configurable: true, + }); + } + + context.with(trace.setSpan(context.active(), 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); + }; + } + + protected getConfirmedPublishPatch( + moduleVersion: string | undefined, + original: Function + ) { + const self = this; + return function confirmedPublish( + this: InstrumentationConsumeChannel, + exchange: string, + routingKey: string, + content: Buffer, + options?: amqp.Options.Publish, + callback?: (err: any, ok: amqp.Replies.Empty) => void + ): boolean { + const channel = this; + const { span, modifiedOptions } = self.createPublishSpan( + self, + exchange, + routingKey, + channel, + moduleVersion, + options + ); + + if (self._config.publishHook) { + safeExecuteInTheMiddle( + () => + self._config.publishHook!(span, { + exchange, + routingKey, + content, + options, + isConfirmChannel: true, + }), + (e) => { + if (e) { + diag.error("amqplib instrumentation: publishHook error", e); + } + }, + true + ); + } + + const patchedOnConfirm = function ( + this: unknown, + err: any, + ok: amqp.Replies.Empty + ) { + try { + callback?.call(this, err, ok); + } finally { + if (self._config.publishConfirmHook) { + safeExecuteInTheMiddle( + () => + self._config.publishConfirmHook!( + span, + { + exchange, + routingKey, + content, + options, + isConfirmChannel: true, + }, + 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(); + } + }; + + // calling confirm channel publish function is storing the message in queue and registering the callback for broker confirm. + // span ends in the patched callback. + 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)); + }; + } + + protected getPublishPatch( + moduleVersion: string | undefined, + original: Function + ) { + const self = this; + return function publish( + this: InstrumentationPublishChannel, + exchange: string, + routingKey: string, + content: Buffer, + options?: amqp.Options.Publish + ): boolean { + if (isConfirmChannelTracing(context.active())) { + // work already done + return original.apply(this, arguments); + } else { + const channel = this; + const { span, modifiedOptions } = self.createPublishSpan( + self, + exchange, + routingKey, + channel, + moduleVersion, + options + ); + + if (self._config.publishHook) { + safeExecuteInTheMiddle( + () => + self._config.publishHook!(span, { + exchange, + routingKey, + content, + options, + isConfirmChannel: false, + }), + (e) => { + if (e) { + diag.error("amqplib instrumentation: publishHook error", e); + } + }, + true + ); + } + + // calling normal channel publish function is only storing the message in queue. + // it does not send it and waits for an ack, so the span duration is expected to be very short. + const argumentsCopy = [...arguments]; + argumentsCopy[3] = modifiedOptions; + const originalRes = original.apply(this, argumentsCopy as any); + span.end(); + return originalRes; + } + }; + } + + private createPublishSpan( + self: this, + exchange: string, + routingKey: string, + channel: InstrumentationPublishChannel, + moduleVersion: string | undefined, + options?: amqp.Options.Publish + ) { + const normalizedExchange = normalizeExchange(exchange); + + const span = self.tracer.startSpan( + `${normalizedExchange} -> ${routingKey} send`, + { + kind: SpanKind.PRODUCER, + attributes: { + ...channel.connection[CONNECTION_ATTRIBUTES], + [SemanticAttributes.MESSAGING_DESTINATION]: exchange, + [SemanticAttributes.MESSAGING_DESTINATION_KIND]: + MessagingDestinationKindValues.TOPIC, + [SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]: routingKey, + [SemanticAttributes.MESSAGING_MESSAGE_ID]: options?.messageId, + [SemanticAttributes.MESSAGING_CONVERSATION_ID]: + options?.correlationId, + }, + } + ); + if (self._config.moduleVersionAttributeName && moduleVersion) { + span.setAttribute(self._config.moduleVersionAttributeName, moduleVersion); + } + const modifiedOptions = options ?? {}; + modifiedOptions.headers = modifiedOptions.headers ?? {}; + + propagation.inject( + trace.setSpan(context.active(), span), + modifiedOptions.headers + ); + + return { span, modifiedOptions }; + } + + private endConsumerSpan( + message: InstrumentationMessage, + isRejected: boolean | null, + operation: EndOperation, + requeue: boolean | undefined + ) { + const storedSpan: Span | undefined = 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(); + delete message[MESSAGE_STORED_SPAN]; + } + + private endAllSpansOnChannel( + channel: InstrumentationConsumeChannel, + isRejected: boolean, + operation: EndOperation, + requeue: boolean | undefined + ) { + const spansNotEnded: { msg: amqp.Message }[] = + channel[CHANNEL_SPANS_NOT_ENDED] ?? []; + spansNotEnded.forEach((msgDetails) => { + this.endConsumerSpan(msgDetails.msg, isRejected, operation, requeue); + }); + Object.defineProperty(channel, CHANNEL_SPANS_NOT_ENDED, { + value: [], + enumerable: false, + configurable: true, + }); + } + + private callConsumeEndHook( + span: Span, + msg: amqp.ConsumeMessage, + rejected: boolean | null, + endOperation: EndOperation + ) { + if (!this._config.consumeEndHook) return; + + safeExecuteInTheMiddle( + () => this._config.consumeEndHook!(span, msg, rejected, endOperation), + (e) => { + if (e) { + diag.error("amqplib instrumentation: consumerEndHook error", e); + } + }, + true + ); + } + + private checkConsumeTimeoutOnChannel(channel: InstrumentationConsumeChannel) { + const currentTime = new Date().getTime(); + const spansNotEnded: { msg: amqp.Message; timeOfConsume: Date }[] = + channel[CHANNEL_SPANS_NOT_ENDED] ?? []; + let i: number; + for (i = 0; i < spansNotEnded.length; i++) { + const currMessage = spansNotEnded[i]; + const timeFromConsume = currentTime - currMessage.timeOfConsume.getTime(); + if (timeFromConsume < this._config.consumeTimeoutMs!) { + break; + } + this.endConsumerSpan( + currMessage.msg, + null, + EndOperation.InstrumentationTimeout, + true + ); + } + spansNotEnded.splice(0, i); + } +} diff --git a/plugins/node/instrumentation-amqplib/src/index.ts b/plugins/node/instrumentation-amqplib/src/index.ts new file mode 100644 index 0000000000..75e9284faa --- /dev/null +++ b/plugins/node/instrumentation-amqplib/src/index.ts @@ -0,0 +1,2 @@ +export * from './amqplib'; +export * from './types'; diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts new file mode 100644 index 0000000000..f2de143723 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -0,0 +1,79 @@ +import { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import type * as amqp from 'amqplib'; + +export interface PublishParams { + exchange: string; + routingKey: string; + content: Buffer; + options?: amqp.Options.Publish; + isConfirmChannel?: boolean; +} + +export interface AmqplibPublishCustomAttributeFunction { + (span: Span, publishParams: PublishParams): void; +} + +export interface AmqplibConfirmCustomAttributeFunction { + (span: Span, publishParams: PublishParams, confirmError: any): void; +} + +export interface AmqplibConsumerCustomAttributeFunction { + (span: Span, msg: amqp.ConsumeMessage): void; +} + +export interface AmqplibConsumerEndCustomAttributeFunction { + (span: Span, msg: amqp.ConsumeMessage, rejected: boolean | null, endOperation: EndOperation): void; +} + +export enum EndOperation { + AutoAck = 'auto ack', + Ack = 'ack', + AckAll = 'ackAll', + Reject = 'reject', + Nack = 'nack', + NackAll = 'nackAll', + ChannelClosed = 'channel closed', + ChannelError = 'channel error', + InstrumentationTimeout = 'instrumentation timeout', +} + +export interface AmqplibInstrumentationConfig extends InstrumentationConfig { + /** hook for adding custom attributes before publish message is sent */ + publishHook?: AmqplibPublishCustomAttributeFunction; + + /** hook for adding custom attributes after publish message is confirmed by the broker */ + publishConfirmHook?: AmqplibConfirmCustomAttributeFunction; + + /** hook for adding custom attributes before consumer message is processed */ + consumeHook?: AmqplibConsumerCustomAttributeFunction; + + /** hook for adding custom attributes after consumer message is acked to server */ + consumeEndHook?: AmqplibConsumerEndCustomAttributeFunction; + + /** + * If passed, a span attribute will be added to all spans with key of the provided "moduleVersionAttributeName" + * and value of the module version. + */ + moduleVersionAttributeName?: string; + + /** + * When user is setting up consume callback, it is user's responsibility to call + * ack/nack etc on the msg to resolve it in the server. + * If user is not calling the ack, the message will stay in the queue until + * channel is closed, or until server timeout expires (if configured). + * While we wait for the ack, a copy of the message is stored in plugin, which + * will never be garbage collected. + * To prevent memory leak, plugin has it's own configuration of timeout, which + * will close the span if user did not call ack after this timeout. + * If timeout is not big enough, span might be closed with 'InstrumentationTimeout', + * and then received valid ack from the user later which will not be instrumented. + * + * Default is 1 minute + */ + consumeTimeoutMs?: number; +} + +export const DEFAULT_CONFIG: AmqplibInstrumentationConfig = { + consumeTimeoutMs: 1000 * 60, // 1 minute +}; diff --git a/plugins/node/instrumentation-amqplib/src/utils.ts b/plugins/node/instrumentation-amqplib/src/utils.ts new file mode 100644 index 0000000000..b48b779b5b --- /dev/null +++ b/plugins/node/instrumentation-amqplib/src/utils.ts @@ -0,0 +1,207 @@ +import { + Context, + createContextKey, + diag, + Span, + SpanAttributes, + SpanAttributeValue, +} from "@opentelemetry/api"; +import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; +import type * as amqp from "amqplib"; + +export const MESSAGE_STORED_SPAN: unique symbol = Symbol( + "opentelemetry.amqplib.message.stored-span" +); +export const CHANNEL_SPANS_NOT_ENDED: unique symbol = Symbol( + "opentelemetry.amqplib.channel.spans-not-ended" +); +export const CHANNEL_CONSUME_TIMEOUT_TIMER: unique symbol = Symbol( + "opentelemetry.amqplib.channel.consumer-timeout-timer" +); +export const CONNECTION_ATTRIBUTES: unique symbol = Symbol( + "opentelemetry.amqplib.connection.attributes" +); + +export type InstrumentationPublishChannel = ( + | amqp.Channel + | amqp.ConfirmChannel +) & { connection: { [CONNECTION_ATTRIBUTES]: SpanAttributes } }; +export type InstrumentationConsumeChannel = amqp.Channel & { + connection: { [CONNECTION_ATTRIBUTES]: SpanAttributes }; + [CHANNEL_SPANS_NOT_ENDED]?: { + msg: amqp.ConsumeMessage; + timeOfConsume: Date; + }[]; + [CHANNEL_CONSUME_TIMEOUT_TIMER]?: NodeJS.Timer; +}; +export type InstrumentationMessage = amqp.Message & { + [MESSAGE_STORED_SPAN]?: Span; +}; + +const IS_CONFIRM_CHANNEL_CONTEXT_KEY: symbol = createContextKey( + "opentelemetry.amqplib.channel.is-confirm-channel" +); + +export const normalizeExchange = (exchangeName: string) => + exchangeName !== "" ? exchangeName : ""; + +const censorPassword = (url: string): string => { + return url.replace(/:[^:@/]*@/, ":***@"); +}; + +const getPort = ( + portFromUrl: number | undefined, + resolvedProtocol: string +): number => { + // we are using the resolved protocol which is upper case + // this code mimic the behavior of the amqplib which is used to set connection params + return portFromUrl || (resolvedProtocol === "AMQP" ? 5672 : 5671); +}; + +const getProtocol = (protocolFromUrl: string | undefined): string => { + const resolvedProtocol = protocolFromUrl || "amqp"; + // the substring removed the ':' part of the protocol ('amqp:' -> 'amqp') + const noEndingColon = resolvedProtocol.endsWith(":") + ? resolvedProtocol.substring(0, resolvedProtocol.length - 1) + : resolvedProtocol; + // upper cases to match spec + return noEndingColon.toUpperCase(); +}; + +const getHostname = (hostnameFromUrl: string | undefined): string => { + // if user supplies empty hostname, it gets forwarded to 'net' package which default it to localhost. + // https://nodejs.org/docs/latest-v12.x/api/net.html#net_socket_connect_options_connectlistener + return hostnameFromUrl || "localhost"; +}; + +const extractConnectionAttributeOrLog = ( + url: string | amqp.Options.Connect, + attributeKey: string, + attributeValue: SpanAttributeValue, + nameForLog: string +): SpanAttributes => { + if (attributeValue) { + return { [attributeKey]: attributeValue }; + } else { + diag.error( + `amqplib instrumentation: could not extract connection attribute ${nameForLog} from user supplied url`, + { + url, + } + ); + return {}; + } +}; + +export const getConnectionAttributesFromServer = ( + conn: amqp.Connection["connection"] +): SpanAttributes => { + const product = conn.serverProperties.product?.toLowerCase?.(); + if (product) { + return { + [SemanticAttributes.MESSAGING_SYSTEM]: product, + }; + } else { + return {}; + } +}; + +export const getConnectionAttributesFromUrl = ( + url: string | amqp.Options.Connect +): SpanAttributes => { + const attributes: SpanAttributes = { + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", // this is the only protocol supported by the instrumented library + }; + + url = url || "amqp://localhost"; + if (typeof url === "object") { + const connectOptions = url as amqp.Options.Connect; + + const protocol = getProtocol(connectOptions?.protocol); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + url, + SemanticAttributes.MESSAGING_PROTOCOL, + protocol, + "protocol" + ), + }); + + const hostname = getHostname(connectOptions?.hostname); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + url, + SemanticAttributes.NET_PEER_NAME, + hostname, + "hostname" + ), + }); + + const port = getPort(connectOptions.port, protocol); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + url, + SemanticAttributes.NET_PEER_PORT, + port, + "port" + ), + }); + } else { + const censoredUrl = censorPassword(url); + attributes[SemanticAttributes.MESSAGING_URL] = censoredUrl; + try { + const urlParts = new URL(censoredUrl); + + const protocol = getProtocol(urlParts.protocol); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + censoredUrl, + SemanticAttributes.MESSAGING_PROTOCOL, + protocol, + "protocol" + ), + }); + + const hostname = getHostname(urlParts.hostname); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + censoredUrl, + SemanticAttributes.NET_PEER_NAME, + hostname, + "hostname" + ), + }); + + const port = getPort(parseInt(urlParts.port), protocol); + Object.assign(attributes, { + ...extractConnectionAttributeOrLog( + censoredUrl, + SemanticAttributes.NET_PEER_PORT, + port, + "port" + ), + }); + } catch (err) { + diag.error( + "amqplib instrumentation: error while extracting connection details from connection url", + { + censoredUrl, + err, + } + ); + } + } + return attributes; +}; + +export const markConfirmChannelTracing = (context: Context) => { + return context.setValue(IS_CONFIRM_CHANNEL_CONTEXT_KEY, true); +}; + +export const unmarkConfirmChannelTracing = (context: Context) => { + return context.deleteValue(IS_CONFIRM_CHANNEL_CONTEXT_KEY); +}; + +export const isConfirmChannelTracing = (context: Context) => { + return context.getValue(IS_CONFIRM_CHANNEL_CONTEXT_KEY) === true; +}; diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts new file mode 100644 index 0000000000..aa88757508 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts @@ -0,0 +1,237 @@ +import 'mocha'; +import * as expect from 'expect'; +import { AmqplibInstrumentation } from '../src'; +import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; + +registerInstrumentationTesting(new AmqplibInstrumentation()); + +import * as amqpCallback from 'amqplib/callback_api'; +import { MessagingDestinationKindValues, SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { context, SpanKind } from '@opentelemetry/api'; +import { asyncConfirmSend, asyncConsume } from './utils'; +import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; + +const msgPayload = 'payload from test'; +const queueName = 'queue-name-from-unittest'; + +describe('amqplib instrumentation callback model', function () { + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + let conn: amqpCallback.Connection; + before((done) => { + amqpCallback.connect(url, (err, connection) => { + conn = connection; + done(); + }); + }); + after((done) => { + conn.close(() => done()); + }); + + describe('channel', () => { + let channel: amqpCallback.Channel; + beforeEach((done) => { + conn.createChannel( + context.bind(context.active(), (err, c) => { + channel = c; + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + channel.on('error', () => {}); + channel.assertQueue( + queueName, + { durable: false }, + context.bind(context.active(), (err, ok) => { + channel.purgeQueue( + queueName, + context.bind(context.active(), (err, ok) => { + done(); + }) + ); + }) + ); + }) + ); + }); + + afterEach((done) => { + try { + channel.close((err) => { + done(); + }); + } catch {} + }); + + it('simple publish and consume from queue callback', (done) => { + const hadSpaceInBuffer = channel.sendToQueue(queueName, Buffer.from(msgPayload)); + expect(hadSpaceInBuffer).toBeTruthy(); + + asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { + noAck: true, + }).then(() => { + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); + + done(); + }); + }); + + it('end span with ack sync', (done) => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]).then(() => { + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + done(); + }); + }); + + it('end span with ack async', (done) => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + asyncConsume(channel, queueName, [ + (msg) => + setTimeout(() => { + channel.ack(msg); + expect(getTestSpans().length).toBe(2); + done(); + }, 1), + ]); + }); + }); + + describe('confirm channel', () => { + let confirmChannel: amqpCallback.ConfirmChannel; + beforeEach((done) => { + conn.createConfirmChannel( + context.bind(context.active(), (err, c) => { + confirmChannel = c; + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + confirmChannel.on('error', () => {}); + confirmChannel.assertQueue( + queueName, + { durable: false }, + context.bind(context.active(), (err, ok) => { + confirmChannel.purgeQueue( + queueName, + context.bind(context.active(), (err, ok) => { + done(); + }) + ); + }) + ); + }) + ); + }); + + afterEach((done) => { + try { + confirmChannel.close((err) => { + done(); + }); + } catch {} + }); + + it('simple publish and consume from queue callback', (done) => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume(confirmChannel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { + noAck: true, + }).then(() => { + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual( + queueName + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual( + queueName + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); + + done(); + }); + }); + }); + + it('end span with ack sync', (done) => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.ack(msg)]).then(() => { + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + done(); + }); + }); + }); + + it('end span with ack async', (done) => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume(confirmChannel, queueName, [ + (msg) => + setTimeout(() => { + confirmChannel.ack(msg); + expect(getTestSpans().length).toBe(2); + done(); + }, 1), + ]); + }); + }); + }); +}); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts new file mode 100644 index 0000000000..6b78e9b375 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts @@ -0,0 +1,105 @@ +import 'mocha'; +import * as expect from 'expect'; +import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { AmqplibInstrumentation } from '../src'; +import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; + +registerInstrumentationTesting(new AmqplibInstrumentation()); +import * as amqp from 'amqplib'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +describe('amqplib instrumentation connection', function () { + describe('connect with url object', () => { + it('should extract connection attributes form url options', async function () { + const testName = this.test!.title; + const conn = await amqp.connect({ + protocol: 'amqp', + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + hostname: TEST_RABBITMQ_HOST, + port: TEST_RABBITMQ_PORT, + }); + + try { + const channel = await conn.createChannel(); + channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); + const [publishSpan] = getTestSpans(); + + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toBeUndefined(); // no url string if value supplied as object + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + } finally { + await conn.close(); + } + }); + + it('should use default protocol', async function () { + const testName = this.test!.title; + const conn = await amqp.connect({ + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + hostname: TEST_RABBITMQ_HOST, + port: TEST_RABBITMQ_PORT, + }); + + try { + const channel = await conn.createChannel(); + channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); + const [publishSpan] = getTestSpans(); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + } finally { + await conn.close(); + } + }); + + it('should use default host', async function () { + if (TEST_RABBITMQ_HOST !== 'localhost') { + return; + } + + const testName = this.test!.title; + const conn = await amqp.connect({ + protocol: 'amqp', + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + port: TEST_RABBITMQ_PORT, + }); + + try { + const channel = await conn.createChannel(); + channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); + const [publishSpan] = getTestSpans(); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + } finally { + await conn.close(); + } + }); + }); + + describe('connect with url string', () => { + it('should extract connection attributes from url options', async function () { + const testName = this.test!.title; + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const conn = await amqp.connect(url); + + try { + const channel = await conn.createChannel(); + channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); + const [publishSpan] = getTestSpans(); + + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + } finally { + await conn.close(); + } + }); + }); +}); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts new file mode 100644 index 0000000000..f678bc4dea --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -0,0 +1,936 @@ +import 'mocha'; +import * as expect from 'expect'; +import * as sinon from 'sinon'; +import * as lodash from 'lodash'; +import { AmqplibInstrumentation, EndOperation, PublishParams } from '../src'; +import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; + +const instrumentation = registerInstrumentationTesting(new AmqplibInstrumentation()); + +import * as amqp from 'amqplib'; +import { ConsumeMessage } from 'amqplib'; +import { MessagingDestinationKindValues, SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from './utils'; +import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { SinonSpy } from 'sinon'; + +const msgPayload = 'payload from test'; +const queueName = 'queue-name-from-unittest'; + +// signal that the channel is closed in test, thus it should not be closed again in afterEach. +// could not find a way to get this from amqplib directly. +const CHANNEL_CLOSED_IN_TEST = Symbol('opentelemetry.amqplib.unittest.channel_closed_in_test'); + +describe('amqplib instrumentation promise model', function () { + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + let conn: amqp.Connection; + before(async () => { + conn = await amqp.connect(url); + }); + after(async () => { + await conn.close(); + }); + + let endHookSpy: SinonSpy; + const expectConsumeEndSpyStatus = (expectedEndOperations: EndOperation[]): void => { + expect(endHookSpy.callCount).toBe(expectedEndOperations.length); + expectedEndOperations.forEach((endOperation: EndOperation, index: number) => { + expect(endHookSpy.args[index][3]).toEqual(endOperation); + switch (endOperation) { + case EndOperation.AutoAck: + case EndOperation.Ack: + case EndOperation.AckAll: + expect(endHookSpy.args[index][2]).toBeFalsy(); + break; + + case EndOperation.Reject: + case EndOperation.Nack: + case EndOperation.NackAll: + case EndOperation.ChannelClosed: + case EndOperation.ChannelError: + expect(endHookSpy.args[index][2]).toBeTruthy(); + break; + } + }); + }; + + describe('channel', () => { + let channel: amqp.Channel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; + beforeEach(async () => { + endHookSpy = sinon.spy(); + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + }); + + channel = await conn.createChannel(); + await channel.assertQueue(queueName, { durable: false }); + await channel.purgeQueue(queueName); + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + channel.on('error', (err: Error) => {}); + }); + afterEach(async () => { + if (!channel[CHANNEL_CLOSED_IN_TEST]) { + try { + await new Promise((resolve) => { + channel.on('close', resolve); + channel.close(); + }); + } catch {} + } + }); + + it('simple publish and consume from queue', async () => { + const hadSpaceInBuffer = channel.sendToQueue(queueName, Buffer.from(msgPayload)); + expect(hadSpaceInBuffer).toBeTruthy(); + + await asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { + noAck: true, + }); + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); + + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); + + describe('ending consume spans', () => { + it('message acked sync', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message acked async', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + // start async timer and ack the message after the callback returns + await new Promise((resolve) => { + asyncConsume(channel, queueName, [ + (msg) => + setTimeout(() => { + channel.ack(msg); + resolve(); + }, 1), + ]); + }); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message nack no requeue', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [(msg) => channel.nack(msg, false, false)]); + await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + const [_, consumerSpan] = getTestSpans(); + expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(consumerSpan.status.message).toEqual('nack called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Nack]); + }); + + it('message nack requeue, then acked', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + // @ts-ignore + await asyncConsume(channel, queueName, [ + (msg: amqp.Message) => channel.nack(msg, false, true), + (msg: amqp.Message) => channel.ack(msg), + ]); + // assert we have the requeued message sent again + expect(getTestSpans().length).toBe(3); + const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); + expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(rejectedConsumerSpan.status.message).toEqual('nack called on message with requeue'); + expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); + }); + + it('ack allUpTo 2 msgs sync', async () => { + lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [ + null, + (msg) => channel.ack(msg, true), + (msg) => channel.ack(msg), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); + }); + + it('nack allUpTo 2 msgs sync', async () => { + lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [ + null, + (msg) => channel.nack(msg, true, false), + (msg) => channel.nack(msg, false, false), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + lodash.range(3, 6).forEach((i) => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual('nack called on message without requeue'); + }); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Nack, EndOperation.Nack]); + }); + + it('ack not in received order', async () => { + lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + const msgs = await asyncConsume(channel, queueName, [null, null, null]); + channel.ack(msgs[1]); + channel.ack(msgs[2]); + channel.ack(msgs[0]); + // assert all 3 span messages are ended + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); + }); + + it('ackAll', async () => { + lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [null, () => channel.ackAll()]); + // assert all 2 span messages are ended by call to ackAll + expect(getTestSpans().length).toBe(4); + expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); + }); + + it('nackAll', async () => { + lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [null, () => channel.nackAll(false)]); + // assert all 2 span messages are ended by calling nackAll + expect(getTestSpans().length).toBe(4); + lodash.range(2, 4).forEach((i) => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual('nackAll called on message without requeue'); + }); + expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); + }); + + it('reject', async () => { + lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [(msg) => channel.reject(msg, false)]); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('reject called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Reject]); + }); + + it('reject with requeue', async () => { + lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + // @ts-ignore + await asyncConsume(channel, queueName, [ + (msg) => channel.reject(msg, true), + (msg) => channel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(3); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('reject called on message with requeue'); + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[2].status.message).toEqual('reject called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); + }); + + it('closing channel should end all open spans on it', async () => { + lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + await new Promise((resolve) => + asyncConsume(channel, queueName, [ + async (msg) => { + await channel.close(); + resolve(); + channel[CHANNEL_CLOSED_IN_TEST] = true; + }, + ]) + ); + + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); + }); + + it('error on channel should end all open spans on it', (done) => { + lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + channel.on('close', () => { + expect(getTestSpans().length).toBe(4); + // second consume ended with valid ack, previous message not acked when channel is errored. + // since we first ack the second message, it appear first in the finished spans array + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); + expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[3].status.message).toEqual('channel error'); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.ChannelError]); + done(); + }); + asyncConsume(channel, queueName, [ + null, + (msg) => { + try { + channel.ack(msg); + channel[CHANNEL_CLOSED_IN_TEST] = true; + // ack the same msg again, this is not valid and should close the channel + channel.ack(msg); + } catch {} + }, + ]); + }); + + it('not acking the message trigger timeout', async () => { + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + consumeTimeoutMs: 1, + }); + + lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); + + await asyncConsume(channel, queueName, [null]); + + // we have timeout of 1 ms, so we wait more than that and check span indeed ended + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); + }); + }); + + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await channel.assertExchange(exchangeName, 'topic', { durable: false }); + + const { queue: queueName } = await channel.assertQueue('', { durable: false }); + await channel.bindQueue(queueName, exchangeName, '#'); + + channel.publish(exchangeName, routingKey, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [null], { + noAck: true, + }); + + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); + }); + }); + + it('moduleVersionAttributeName works with publish and consume', async () => { + const VERSION_ATTR = 'module.version'; + instrumentation.setConfig({ + moduleVersionAttributeName: VERSION_ATTR, + }); + + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach((s) => expect(s.attributes[VERSION_ATTR]).toMatch(/\d{1,4}\.\d{1,4}\.\d{1,5}.*/)); + }); + + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + const attributeNameFromEndHook = 'attribute.name.from.endhook'; + const endHookAttributeValue = 'attribute value from end hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(msg!.content.toString()).toEqual(msgPayload); + }, + consumeEndHook: ( + span: Span, + msg: amqp.ConsumeMessage | null, + rejected: boolean | null, + endOperation: EndOperation + ): void => { + span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); + expect(endOperation).toEqual(EndOperation.AutoAck); + }, + }); + + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); + expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); + expect(getTestSpans()[1].attributes[attributeNameFromEndHook]).toEqual(endHookAttributeValue); + }); + + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + }); + + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual(hookAttributeValue)); + }); + }); + + describe('delete queue', () => { + it('consumer receives null msg when a queue is deleted in broker', async () => { + const queueNameForDeletion = 'queue-to-be-deleted'; + await channel.assertQueue(queueNameForDeletion, { durable: false }); + await channel.purgeQueue(queueNameForDeletion); + + await channel.consume(queueNameForDeletion, (msg: ConsumeMessage | null) => {}, { noAck: true }); + await channel.deleteQueue(queueNameForDeletion); + }); + }); + }); + + describe('confirm channel', () => { + let confirmChannel: amqp.ConfirmChannel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; + beforeEach(async () => { + endHookSpy = sinon.spy(); + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + }); + + confirmChannel = await conn.createConfirmChannel(); + await confirmChannel.assertQueue(queueName, { durable: false }); + await confirmChannel.purgeQueue(queueName); + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + confirmChannel.on('error', (err: Error) => {}); + }); + afterEach(async () => { + if (!confirmChannel[CHANNEL_CLOSED_IN_TEST]) { + try { + await new Promise((resolve) => { + confirmChannel.on('close', resolve); + confirmChannel.close(); + }); + } catch {} + } + }); + + it('simple publish with confirm and consume from queue', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume( + confirmChannel, + queueName, + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); + + it('confirm throw should not affect span end', async () => { + const confirmUserError = new Error('callback error'); + await asyncConfirmSend(confirmChannel, queueName, msgPayload, () => { + throw confirmUserError; + }).catch((reject) => expect(reject).toEqual(confirmUserError)); + + await asyncConsume( + confirmChannel, + queueName, + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + + expect(getTestSpans()).toHaveLength(2); + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); + + describe('ending consume spans', () => { + it('message acked sync', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.ack(msg)]); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message acked async', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + // start async timer and ack the message after the callback returns + await new Promise((resolve) => { + asyncConsume(confirmChannel, queueName, [ + (msg) => + setTimeout(() => { + confirmChannel.ack(msg); + resolve(); + }, 1), + ]); + }); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message nack no requeue', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.nack(msg, false, false)]); + await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + const [_, consumerSpan] = getTestSpans(); + expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(consumerSpan.status.message).toEqual('nack called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Nack]); + }); + + it('message nack requeue, then acked', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [ + (msg: amqp.Message) => confirmChannel.nack(msg, false, true), + (msg: amqp.Message) => confirmChannel.ack(msg), + ]); + // assert we have the requeued message sent again + expect(getTestSpans().length).toBe(3); + const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); + expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(rejectedConsumerSpan.status.message).toEqual('nack called on message with requeue'); + expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); + }); + + it('ack allUpTo 2 msgs sync', async () => { + await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [ + null, + (msg) => confirmChannel.ack(msg, true), + (msg) => confirmChannel.ack(msg), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); + }); + + it('nack allUpTo 2 msgs sync', async () => { + await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [ + null, + (msg) => confirmChannel.nack(msg, true, false), + (msg) => confirmChannel.nack(msg, false, false), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + lodash.range(3, 6).forEach((i) => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual('nack called on message without requeue'); + }); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Nack, EndOperation.Nack]); + }); + + it('ack not in received order', async () => { + await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + const msgs = await asyncConsume(confirmChannel, queueName, [null, null, null]); + confirmChannel.ack(msgs[1]); + confirmChannel.ack(msgs[2]); + confirmChannel.ack(msgs[0]); + // assert all 3 span messages are ended + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); + }); + + it('ackAll', async () => { + await Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [null, () => confirmChannel.ackAll()]); + // assert all 2 span messages are ended by call to ackAll + expect(getTestSpans().length).toBe(4); + expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); + }); + + it('nackAll', async () => { + await Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [null, () => confirmChannel.nackAll(false)]); + // assert all 2 span messages are ended by calling nackAll + expect(getTestSpans().length).toBe(4); + lodash.range(2, 4).forEach((i) => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual('nackAll called on message without requeue'); + }); + expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); + }); + + it('reject', async () => { + await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.reject(msg, false)]); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('reject called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Reject]); + }); + + it('reject with requeue', async () => { + await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + // @ts-ignore + await asyncConsume(confirmChannel, queueName, [ + (msg) => confirmChannel.reject(msg, true), + (msg) => confirmChannel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(3); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('reject called on message with requeue'); + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[2].status.message).toEqual('reject called on message without requeue'); + expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); + }); + + it('closing channel should end all open spans on it', async () => { + await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + await new Promise((resolve) => + asyncConsume(confirmChannel, queueName, [ + async (msg) => { + await confirmChannel.close(); + resolve(); + confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; + }, + ]) + ); + + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); + }); + + it('error on channel should end all open spans on it', (done) => { + Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))).then(() => { + confirmChannel.on('close', () => { + expect(getTestSpans().length).toBe(4); + // second consume ended with valid ack, previous message not acked when channel is errored. + // since we first ack the second message, it appear first in the finished spans array + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); + expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[3].status.message).toEqual('channel error'); + expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.ChannelError]); + done(); + }); + asyncConsume(confirmChannel, queueName, [ + null, + (msg) => { + try { + confirmChannel.ack(msg); + confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; + // ack the same msg again, this is not valid and should close the channel + confirmChannel.ack(msg); + } catch {} + }, + ]); + }); + }); + + it('not acking the message trigger timeout', async () => { + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + consumeTimeoutMs: 1, + }); + + await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); + + await asyncConsume(confirmChannel, queueName, [null]); + + // we have timeout of 1 ms, so we wait more than that and check span indeed ended + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); + }); + }); + + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await confirmChannel.assertExchange(exchangeName, 'topic', { durable: false }); + + const { queue: queueName } = await confirmChannel.assertQueue('', { durable: false }); + await confirmChannel.bindQueue(queueName, exchangeName, '#'); + + await asyncConfirmPublish(confirmChannel, exchangeName, routingKey, msgPayload); + + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, + }); + + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( + MessagingDestinationKindValues.TOPIC + ); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); + }); + }); + + it('moduleVersionAttributeName works with publish and consume', async () => { + const VERSION_ATTR = 'module.version'; + instrumentation.setConfig({ + moduleVersionAttributeName: VERSION_ATTR, + }); + + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume( + confirmChannel, + queueName, + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach((s) => expect(s.attributes[VERSION_ATTR]).toMatch(/\d{1,4}\.\d{1,4}\.\d{1,5}.*/)); + }); + + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + const attributeNameFromConfirmEndHook = 'attribute.name.from.confirm.endhook'; + const confirmEndHookAttributeValue = 'attribute value from confirm end hook'; + const attributeNameFromConsumeEndHook = 'attribute.name.from.consume.endhook'; + const consumeEndHookAttributeValue = 'attribute value from consume end hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams) => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + expect(publishParams.isConfirmChannel).toBe(true); + }, + publishConfirmHook: (span, publishParams) => { + span.setAttribute(attributeNameFromConfirmEndHook, confirmEndHookAttributeValue); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null) => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(msg!.content.toString()).toEqual(msgPayload); + }, + consumeEndHook: ( + span: Span, + msg: amqp.ConsumeMessage | null, + rejected: boolean | null, + endOperation: EndOperation + ): void => { + span.setAttribute(attributeNameFromConsumeEndHook, consumeEndHookAttributeValue); + expect(endOperation).toEqual(EndOperation.AutoAck); + }, + }); + + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); + expect(getTestSpans()[0].attributes[attributeNameFromConfirmEndHook]).toEqual( + confirmEndHookAttributeValue + ); + expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); + expect(getTestSpans()[1].attributes[attributeNameFromConsumeEndHook]).toEqual( + consumeEndHookAttributeValue + ); + }); + + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + publishConfirmHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + }); + + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual(hookAttributeValue)); + }); + }); + }); +}); diff --git a/plugins/node/instrumentation-amqplib/test/config.ts b/plugins/node/instrumentation-amqplib/test/config.ts new file mode 100644 index 0000000000..2429e8fc81 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/config.ts @@ -0,0 +1,7 @@ +// should match the values used to start the docker for tests: +// 1. 'test:docker:run' script in package.json +// 2. services in github actions workflow +export const TEST_RABBITMQ_HOST = 'localhost'; +export const TEST_RABBITMQ_PORT = 22221; +export const TEST_RABBITMQ_USER = 'username'; +export const TEST_RABBITMQ_PASS = 'password'; diff --git a/plugins/node/instrumentation-amqplib/test/utils.test.ts b/plugins/node/instrumentation-amqplib/test/utils.test.ts new file mode 100644 index 0000000000..cd44e53268 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/utils.test.ts @@ -0,0 +1,152 @@ +import "mocha"; +import * as expect from "expect"; +import { getConnectionAttributesFromServer, getConnectionAttributesFromUrl } from "../src/utils"; +import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; +import * as amqp from "amqplib"; +import { rabbitMqUrl } from "./utils"; + +describe("utils", function () { + describe("getConnectionAttributesFromServer", function () { + + let conn: amqp.Connection; + before(async () => { + conn = await amqp.connect(rabbitMqUrl); + }); + after(async () => { + await conn.close(); + }); + + it("messaging system attribute", function () { + const attributes = getConnectionAttributesFromServer(conn.connection); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_SYSTEM]: 'rabbitmq', + }); + }); + }); + + describe("getConnectionAttributesFromUrl", function () { + it("all features", function () { + const attributes = getConnectionAttributesFromUrl( + `amqp://user:pass@host:10000/vhost` + ); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.NET_PEER_PORT]: 10000, + [SemanticAttributes.MESSAGING_URL]: `amqp://user:***@host:10000/vhost`, + }); + }); + + it("all features encoded", function () { + const attributes = getConnectionAttributesFromUrl( + `amqp://user%61:%61pass@ho%61st:10000/v%2fhost` + ); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "ho%61st", + [SemanticAttributes.NET_PEER_PORT]: 10000, + [SemanticAttributes.MESSAGING_URL]: `amqp://user%61:***@ho%61st:10000/v%2fhost`, + }); + }); + + it("only protocol", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "localhost", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp://`, + }); + }); + + it("empty username and password", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://:@/`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.MESSAGING_URL]: `amqp://:***@/`, + }); + }); + + it("username and no password", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://user@`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.MESSAGING_URL]: `amqp://user@`, + }); + }); + + it("username and password, no host", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://user:pass@`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.MESSAGING_URL]: `amqp://user:***@`, + }); + }); + + it("host only", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://host`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp://host`, + }); + }); + + it("port only", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://:10000`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.MESSAGING_URL]: `amqp://:10000`, + }); + }); + + it("vhost only", function () { + const attributes = getConnectionAttributesFromUrl(`amqp:///vhost`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "localhost", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp:///vhost`, + }); + }); + + it("host only, trailing slash", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://host/`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp://host/`, + }); + }); + + it("vhost encoded", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://host/%2f`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp://host/%2f`, + }); + }); + + it("IPv6 host", function () { + const attributes = getConnectionAttributesFromUrl(`amqp://[::1]`); + expect(attributes).toStrictEqual({ + [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", + [SemanticAttributes.NET_PEER_NAME]: "[::1]", + [SemanticAttributes.NET_PEER_PORT]: 5672, + [SemanticAttributes.MESSAGING_URL]: `amqp://[::1]`, + }); + }); + }); +}); diff --git a/plugins/node/instrumentation-amqplib/test/utils.ts b/plugins/node/instrumentation-amqplib/test/utils.ts new file mode 100644 index 0000000000..b5f9da8d86 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/test/utils.ts @@ -0,0 +1,73 @@ +import type * as amqp from 'amqplib'; +import type * as amqpCallback from 'amqplib/callback_api'; +import * as expect from 'expect'; +import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; + +export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + +export const asyncConfirmSend = ( + confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, + queueName: string, + msgPayload: string, + callback?: () => void +): Promise => { + return new Promise((resolve, reject) => { + const hadSpaceInBuffer = confirmChannel.sendToQueue(queueName, Buffer.from(msgPayload), {}, (err) => { + try { + callback?.(); + resolve(); + } catch (e) { + reject(e); + } + }); + expect(hadSpaceInBuffer).toBeTruthy(); + }); +}; + +export const asyncConfirmPublish = ( + confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, + exchange: string, + routingKey: string, + msgPayload: string, + callback?: () => void +): Promise => { + return new Promise((resolve, reject) => { + const hadSpaceInBuffer = confirmChannel.publish(exchange, routingKey, Buffer.from(msgPayload), {}, (err) => { + try { + callback?.(); + resolve(); + } catch (e) { + reject(e); + } + }); + expect(hadSpaceInBuffer).toBeTruthy(); + }); +}; + +export const asyncConsume = ( + channel: amqp.Channel | amqpCallback.Channel | amqp.ConfirmChannel | amqpCallback.ConfirmChannel, + queueName: string, + callback: ( ((msg: amqp.Message) => unknown) | null)[], + options?: amqp.Options.Consume +): Promise => { + const msgs: amqp.Message[] = []; + return new Promise((resolve) => + channel.consume( + queueName, + (msg) => { + if(!msg) { throw Error('received null msg')} + msgs.push(msg); + try { + callback[msgs.length - 1]?.(msg); + if (msgs.length >= callback.length) { + setImmediate(() => resolve(msgs)); + } + } catch (err) { + setImmediate(() => resolve(msgs)); + throw err; + } + }, + options + ) + ); +}; diff --git a/plugins/node/instrumentation-amqplib/tsconfig.json b/plugins/node/instrumentation-amqplib/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} From 0d39760a545bff9e80c3ceb13cd57bb7acbc57ae Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:20:36 +0200 Subject: [PATCH 02/39] chore(amqplib): lint fix --- .../instrumentation-amqplib/.eslintignore | 1 + .../node/instrumentation-amqplib/.eslintrc.js | 7 + .../node/instrumentation-amqplib/.npmignore | 4 + .../instrumentation-amqplib/src/amqplib.ts | 125 +- .../node/instrumentation-amqplib/src/index.ts | 15 + .../node/instrumentation-amqplib/src/types.ts | 114 +- .../node/instrumentation-amqplib/src/utils.ts | 77 +- .../test/amqplib-callbacks.test.ts | 531 +++-- .../test/amqplib-connection.test.ts | 231 +- .../test/amqplib-promise.test.ts | 2114 ++++++++++------- .../instrumentation-amqplib/test/config.ts | 18 +- .../test/utils.test.ts | 174 +- .../instrumentation-amqplib/test/utils.ts | 151 +- 13 files changed, 2131 insertions(+), 1431 deletions(-) create mode 100644 plugins/node/instrumentation-amqplib/.eslintignore create mode 100644 plugins/node/instrumentation-amqplib/.eslintrc.js create mode 100644 plugins/node/instrumentation-amqplib/.npmignore diff --git a/plugins/node/instrumentation-amqplib/.eslintignore b/plugins/node/instrumentation-amqplib/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/instrumentation-amqplib/.eslintrc.js b/plugins/node/instrumentation-amqplib/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/instrumentation-amqplib/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/instrumentation-amqplib/.npmignore b/plugins/node/instrumentation-amqplib/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 378707f415..8992269545 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -1,3 +1,18 @@ +/* + * 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. + */ import { context, diag, @@ -6,7 +21,7 @@ import { Span, SpanKind, SpanStatusCode, -} from "@opentelemetry/api"; +} from '@opentelemetry/api'; import { InstrumentationBase, InstrumentationModuleDefinition, @@ -14,18 +29,18 @@ import { InstrumentationNodeModuleFile, isWrapped, safeExecuteInTheMiddle, -} from "@opentelemetry/instrumentation"; +} from '@opentelemetry/instrumentation'; import { SemanticAttributes, MessagingOperationValues, MessagingDestinationKindValues, -} from "@opentelemetry/semantic-conventions"; -import type * as amqp from "amqplib"; +} from '@opentelemetry/semantic-conventions'; +import type * as amqp from 'amqplib'; import { AmqplibInstrumentationConfig, DEFAULT_CONFIG, EndOperation, -} from "./types"; +} from './types'; import { CHANNEL_CONSUME_TIMEOUT_TIMER, CHANNEL_SPANS_NOT_ENDED, @@ -40,15 +55,15 @@ import { MESSAGE_STORED_SPAN, normalizeExchange, unmarkConfirmChannelTracing, -} from "./utils"; -import { VERSION } from "./version"; +} from './utils'; +import { VERSION } from './version'; export class AmqplibInstrumentation extends InstrumentationBase { protected override _config!: AmqplibInstrumentationConfig; constructor(config: AmqplibInstrumentationConfig = {}) { super( - "@opentelemetry/instrumentation-amqplib", + '@opentelemetry/instrumentation-amqplib', VERSION, Object.assign({}, DEFAULT_CONFIG, config) ); @@ -61,30 +76,30 @@ export class AmqplibInstrumentation extends InstrumentationBase { protected init(): InstrumentationModuleDefinition { const channelModelModuleFile = new InstrumentationNodeModuleFile( - `amqplib/lib/channel_model.js`, - [">=0.5.5"], + 'amqplib/lib/channel_model.js', + ['>=0.5.5'], this.patchChannelModel.bind(this), this.unpatchChannelModel.bind(this) ); const callbackModelModuleFile = new InstrumentationNodeModuleFile( - `amqplib/lib/callback_model.js`, - [">=0.5.5"], + 'amqplib/lib/callback_model.js', + ['>=0.5.5'], this.patchChannelModel.bind(this), this.unpatchChannelModel.bind(this) ); const connectModuleFile = new InstrumentationNodeModuleFile( - `amqplib/lib/connect.js`, - [">=0.5.5"], + 'amqplib/lib/connect.js', + ['>=0.5.5'], this.patchConnect.bind(this), this.unpatchConnect.bind(this) ); const module = new InstrumentationNodeModuleDefinition( - "amqplib", - [">=0.5.5"], + 'amqplib', + ['>=0.5.5'], undefined, undefined, [channelModelModuleFile, connectModuleFile, callbackModelModuleFile] @@ -95,14 +110,14 @@ export class AmqplibInstrumentation extends InstrumentationBase { private patchConnect(moduleExports: any) { moduleExports = this.unpatchConnect(moduleExports); if (!isWrapped(moduleExports.connect)) { - this._wrap(moduleExports, "connect", this.getConnectPatch.bind(this)); + this._wrap(moduleExports, 'connect', this.getConnectPatch.bind(this)); } return moduleExports; } private unpatchConnect(moduleExports: any) { if (isWrapped(moduleExports.connect)) { - this._unwrap(moduleExports, "connect"); + this._unwrap(moduleExports, 'connect'); } return moduleExports; } @@ -114,63 +129,63 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (!isWrapped(moduleExports.Channel.prototype.publish)) { this._wrap( moduleExports.Channel.prototype, - "publish", + 'publish', this.getPublishPatch.bind(this, moduleVersion) ); } if (!isWrapped(moduleExports.Channel.prototype.consume)) { this._wrap( moduleExports.Channel.prototype, - "consume", + 'consume', this.getConsumePatch.bind(this, moduleVersion) ); } if (!isWrapped(moduleExports.Channel.prototype.ack)) { this._wrap( moduleExports.Channel.prototype, - "ack", + 'ack', this.getAckPatch.bind(this, false, EndOperation.Ack) ); } if (!isWrapped(moduleExports.Channel.prototype.nack)) { this._wrap( moduleExports.Channel.prototype, - "nack", + 'nack', this.getAckPatch.bind(this, true, EndOperation.Nack) ); } if (!isWrapped(moduleExports.Channel.prototype.reject)) { this._wrap( moduleExports.Channel.prototype, - "reject", + 'reject', this.getAckPatch.bind(this, true, EndOperation.Reject) ); } if (!isWrapped(moduleExports.Channel.prototype.ackAll)) { this._wrap( moduleExports.Channel.prototype, - "ackAll", + 'ackAll', this.getAckAllPatch.bind(this, false, EndOperation.AckAll) ); } if (!isWrapped(moduleExports.Channel.prototype.nackAll)) { this._wrap( moduleExports.Channel.prototype, - "nackAll", + 'nackAll', this.getAckAllPatch.bind(this, true, EndOperation.NackAll) ); } if (!isWrapped(moduleExports.Channel.prototype.emit)) { this._wrap( moduleExports.Channel.prototype, - "emit", + 'emit', this.getChannelEmitPatch.bind(this) ); } if (!isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { this._wrap( moduleExports.ConfirmChannel.prototype, - "publish", + 'publish', this.getConfirmedPublishPatch.bind(this, moduleVersion) ); } @@ -179,31 +194,31 @@ export class AmqplibInstrumentation extends InstrumentationBase { private unpatchChannelModel(moduleExports: any) { if (isWrapped(moduleExports.Channel.prototype.publish)) { - this._unwrap(moduleExports.Channel.prototype, "publish"); + this._unwrap(moduleExports.Channel.prototype, 'publish'); } if (isWrapped(moduleExports.Channel.prototype.consume)) { - this._unwrap(moduleExports.Channel.prototype, "consume"); + this._unwrap(moduleExports.Channel.prototype, 'consume'); } if (isWrapped(moduleExports.Channel.prototype.ack)) { - this._unwrap(moduleExports.Channel.prototype, "ack"); + this._unwrap(moduleExports.Channel.prototype, 'ack'); } if (isWrapped(moduleExports.Channel.prototype.nack)) { - this._unwrap(moduleExports.Channel.prototype, "nack"); + this._unwrap(moduleExports.Channel.prototype, 'nack'); } if (isWrapped(moduleExports.Channel.prototype.reject)) { - this._unwrap(moduleExports.Channel.prototype, "reject"); + this._unwrap(moduleExports.Channel.prototype, 'reject'); } if (isWrapped(moduleExports.Channel.prototype.ackAll)) { - this._unwrap(moduleExports.Channel.prototype, "ackAll"); + this._unwrap(moduleExports.Channel.prototype, 'ackAll'); } if (isWrapped(moduleExports.Channel.prototype.nackAll)) { - this._unwrap(moduleExports.Channel.prototype, "nackAll"); + this._unwrap(moduleExports.Channel.prototype, 'nackAll'); } if (isWrapped(moduleExports.Channel.prototype.emit)) { - this._unwrap(moduleExports.Channel.prototype, "emit"); + this._unwrap(moduleExports.Channel.prototype, 'emit'); } if (isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { - this._unwrap(moduleExports.ConfirmChannel.prototype, "publish"); + this._unwrap(moduleExports.ConfirmChannel.prototype, 'publish'); } return moduleExports; } @@ -253,7 +268,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { this: InstrumentationConsumeChannel, eventName: string ) { - if (eventName === "close") { + if (eventName === 'close') { self.endAllSpansOnChannel( this, true, @@ -265,7 +280,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { clearInterval(activeTimer); } delete this[CHANNEL_CONSUME_TIMEOUT_TIMER]; - } else if (eventName === "error") { + } else if (eventName === 'error') { self.endAllSpansOnChannel( this, true, @@ -312,7 +327,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { const spansNotEnded: { msg: amqp.Message }[] = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; const msgIndex = spansNotEnded.findIndex( - (msgDetails) => msgDetails.msg === message + msgDetails => msgDetails.msg === message ); if (msgIndex < 0) { // should not happen in happy flow @@ -358,7 +373,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { options?: amqp.Options.Consume ): Promise { const channel = this; - if (!channel.hasOwnProperty(CHANNEL_SPANS_NOT_ENDED)) { + if ( + !Object.prototype.hasOwnProperty.call(channel, CHANNEL_SPANS_NOT_ENDED) + ) { if (self._config.consumeTimeoutMs) { const timer = setInterval(() => { self.checkConsumeTimeoutOnChannel(channel); @@ -423,9 +440,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (self._config.consumeHook) { safeExecuteInTheMiddle( () => self._config.consumeHook!(span, msg), - (e) => { + e => { if (e) { - diag.error("amqplib instrumentation: consumerHook error", e); + diag.error('amqplib instrumentation: consumerHook error', e); } }, true @@ -494,9 +511,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { options, isConfirmChannel: true, }), - (e) => { + e => { if (e) { - diag.error("amqplib instrumentation: publishHook error", e); + diag.error('amqplib instrumentation: publishHook error', e); } }, true @@ -525,10 +542,10 @@ export class AmqplibInstrumentation extends InstrumentationBase { }, err ), - (e) => { + e => { if (e) { diag.error( - "amqplib instrumentation: publishConfirmHook error", + 'amqplib instrumentation: publishConfirmHook error', e ); } @@ -596,9 +613,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { options, isConfirmChannel: false, }), - (e) => { + e => { if (e) { - diag.error("amqplib instrumentation: publishHook error", e); + diag.error('amqplib instrumentation: publishHook error', e); } }, true @@ -672,10 +689,10 @@ export class AmqplibInstrumentation extends InstrumentationBase { operation !== EndOperation.ChannelError ? `${operation} called on message${ requeue === true - ? " with requeue" + ? ' with requeue' : requeue === false - ? " without requeue" - : "" + ? ' without requeue' + : '' }` : operation, }); @@ -693,7 +710,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { ) { const spansNotEnded: { msg: amqp.Message }[] = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; - spansNotEnded.forEach((msgDetails) => { + spansNotEnded.forEach(msgDetails => { this.endConsumerSpan(msgDetails.msg, isRejected, operation, requeue); }); Object.defineProperty(channel, CHANNEL_SPANS_NOT_ENDED, { @@ -713,9 +730,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { safeExecuteInTheMiddle( () => this._config.consumeEndHook!(span, msg, rejected, endOperation), - (e) => { + e => { if (e) { - diag.error("amqplib instrumentation: consumerEndHook error", e); + diag.error('amqplib instrumentation: consumerEndHook error', e); } }, true diff --git a/plugins/node/instrumentation-amqplib/src/index.ts b/plugins/node/instrumentation-amqplib/src/index.ts index 75e9284faa..1fe4b66e4d 100644 --- a/plugins/node/instrumentation-amqplib/src/index.ts +++ b/plugins/node/instrumentation-amqplib/src/index.ts @@ -1,2 +1,17 @@ +/* + * 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. + */ export * from './amqplib'; export * from './types'; diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts index f2de143723..bc2022d8f4 100644 --- a/plugins/node/instrumentation-amqplib/src/types.ts +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -1,79 +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. + */ import { Span } from '@opentelemetry/api'; import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type * as amqp from 'amqplib'; export interface PublishParams { - exchange: string; - routingKey: string; - content: Buffer; - options?: amqp.Options.Publish; - isConfirmChannel?: boolean; + exchange: string; + routingKey: string; + content: Buffer; + options?: amqp.Options.Publish; + isConfirmChannel?: boolean; } export interface AmqplibPublishCustomAttributeFunction { - (span: Span, publishParams: PublishParams): void; + (span: Span, publishParams: PublishParams): void; } export interface AmqplibConfirmCustomAttributeFunction { - (span: Span, publishParams: PublishParams, confirmError: any): void; + (span: Span, publishParams: PublishParams, confirmError: any): void; } export interface AmqplibConsumerCustomAttributeFunction { - (span: Span, msg: amqp.ConsumeMessage): void; + (span: Span, msg: amqp.ConsumeMessage): void; } export interface AmqplibConsumerEndCustomAttributeFunction { - (span: Span, msg: amqp.ConsumeMessage, rejected: boolean | null, endOperation: EndOperation): void; + ( + span: Span, + msg: amqp.ConsumeMessage, + rejected: boolean | null, + endOperation: EndOperation + ): void; } export enum EndOperation { - AutoAck = 'auto ack', - Ack = 'ack', - AckAll = 'ackAll', - Reject = 'reject', - Nack = 'nack', - NackAll = 'nackAll', - ChannelClosed = 'channel closed', - ChannelError = 'channel error', - InstrumentationTimeout = 'instrumentation timeout', + AutoAck = 'auto ack', + Ack = 'ack', + AckAll = 'ackAll', + Reject = 'reject', + Nack = 'nack', + NackAll = 'nackAll', + ChannelClosed = 'channel closed', + ChannelError = 'channel error', + InstrumentationTimeout = 'instrumentation timeout', } export interface AmqplibInstrumentationConfig extends InstrumentationConfig { - /** hook for adding custom attributes before publish message is sent */ - publishHook?: AmqplibPublishCustomAttributeFunction; + /** hook for adding custom attributes before publish message is sent */ + publishHook?: AmqplibPublishCustomAttributeFunction; - /** hook for adding custom attributes after publish message is confirmed by the broker */ - publishConfirmHook?: AmqplibConfirmCustomAttributeFunction; + /** hook for adding custom attributes after publish message is confirmed by the broker */ + publishConfirmHook?: AmqplibConfirmCustomAttributeFunction; - /** hook for adding custom attributes before consumer message is processed */ - consumeHook?: AmqplibConsumerCustomAttributeFunction; + /** hook for adding custom attributes before consumer message is processed */ + consumeHook?: AmqplibConsumerCustomAttributeFunction; - /** hook for adding custom attributes after consumer message is acked to server */ - consumeEndHook?: AmqplibConsumerEndCustomAttributeFunction; + /** hook for adding custom attributes after consumer message is acked to server */ + consumeEndHook?: AmqplibConsumerEndCustomAttributeFunction; - /** - * If passed, a span attribute will be added to all spans with key of the provided "moduleVersionAttributeName" - * and value of the module version. - */ - moduleVersionAttributeName?: string; + /** + * If passed, a span attribute will be added to all spans with key of the provided "moduleVersionAttributeName" + * and value of the module version. + */ + moduleVersionAttributeName?: string; - /** - * When user is setting up consume callback, it is user's responsibility to call - * ack/nack etc on the msg to resolve it in the server. - * If user is not calling the ack, the message will stay in the queue until - * channel is closed, or until server timeout expires (if configured). - * While we wait for the ack, a copy of the message is stored in plugin, which - * will never be garbage collected. - * To prevent memory leak, plugin has it's own configuration of timeout, which - * will close the span if user did not call ack after this timeout. - * If timeout is not big enough, span might be closed with 'InstrumentationTimeout', - * and then received valid ack from the user later which will not be instrumented. - * - * Default is 1 minute - */ - consumeTimeoutMs?: number; + /** + * When user is setting up consume callback, it is user's responsibility to call + * ack/nack etc on the msg to resolve it in the server. + * If user is not calling the ack, the message will stay in the queue until + * channel is closed, or until server timeout expires (if configured). + * While we wait for the ack, a copy of the message is stored in plugin, which + * will never be garbage collected. + * To prevent memory leak, plugin has it's own configuration of timeout, which + * will close the span if user did not call ack after this timeout. + * If timeout is not big enough, span might be closed with 'InstrumentationTimeout', + * and then received valid ack from the user later which will not be instrumented. + * + * Default is 1 minute + */ + consumeTimeoutMs?: number; } export const DEFAULT_CONFIG: AmqplibInstrumentationConfig = { - consumeTimeoutMs: 1000 * 60, // 1 minute + consumeTimeoutMs: 1000 * 60, // 1 minute }; diff --git a/plugins/node/instrumentation-amqplib/src/utils.ts b/plugins/node/instrumentation-amqplib/src/utils.ts index b48b779b5b..85e9cbcba0 100644 --- a/plugins/node/instrumentation-amqplib/src/utils.ts +++ b/plugins/node/instrumentation-amqplib/src/utils.ts @@ -1,3 +1,18 @@ +/* + * 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. + */ import { Context, createContextKey, @@ -5,21 +20,22 @@ import { Span, SpanAttributes, SpanAttributeValue, -} from "@opentelemetry/api"; -import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; -import type * as amqp from "amqplib"; +} from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type * as amqp from 'amqplib'; +import * as urlLib from 'url'; export const MESSAGE_STORED_SPAN: unique symbol = Symbol( - "opentelemetry.amqplib.message.stored-span" + 'opentelemetry.amqplib.message.stored-span' ); export const CHANNEL_SPANS_NOT_ENDED: unique symbol = Symbol( - "opentelemetry.amqplib.channel.spans-not-ended" + 'opentelemetry.amqplib.channel.spans-not-ended' ); export const CHANNEL_CONSUME_TIMEOUT_TIMER: unique symbol = Symbol( - "opentelemetry.amqplib.channel.consumer-timeout-timer" + 'opentelemetry.amqplib.channel.consumer-timeout-timer' ); export const CONNECTION_ATTRIBUTES: unique symbol = Symbol( - "opentelemetry.amqplib.connection.attributes" + 'opentelemetry.amqplib.connection.attributes' ); export type InstrumentationPublishChannel = ( @@ -39,14 +55,14 @@ export type InstrumentationMessage = amqp.Message & { }; const IS_CONFIRM_CHANNEL_CONTEXT_KEY: symbol = createContextKey( - "opentelemetry.amqplib.channel.is-confirm-channel" + 'opentelemetry.amqplib.channel.is-confirm-channel' ); export const normalizeExchange = (exchangeName: string) => - exchangeName !== "" ? exchangeName : ""; + exchangeName !== '' ? exchangeName : ''; const censorPassword = (url: string): string => { - return url.replace(/:[^:@/]*@/, ":***@"); + return url.replace(/:[^:@/]*@/, ':***@'); }; const getPort = ( @@ -55,23 +71,23 @@ const getPort = ( ): number => { // we are using the resolved protocol which is upper case // this code mimic the behavior of the amqplib which is used to set connection params - return portFromUrl || (resolvedProtocol === "AMQP" ? 5672 : 5671); + return portFromUrl || (resolvedProtocol === 'AMQP' ? 5672 : 5671); }; -const getProtocol = (protocolFromUrl: string | undefined): string => { - const resolvedProtocol = protocolFromUrl || "amqp"; +const getProtocol = (protocolFromUrl: string | null): string => { + const resolvedProtocol = protocolFromUrl || 'amqp'; // the substring removed the ':' part of the protocol ('amqp:' -> 'amqp') - const noEndingColon = resolvedProtocol.endsWith(":") + const noEndingColon = resolvedProtocol.endsWith(':') ? resolvedProtocol.substring(0, resolvedProtocol.length - 1) : resolvedProtocol; // upper cases to match spec return noEndingColon.toUpperCase(); }; -const getHostname = (hostnameFromUrl: string | undefined): string => { +const getHostname = (hostnameFromUrl: string | null): string => { // if user supplies empty hostname, it gets forwarded to 'net' package which default it to localhost. // https://nodejs.org/docs/latest-v12.x/api/net.html#net_socket_connect_options_connectlistener - return hostnameFromUrl || "localhost"; + return hostnameFromUrl || 'localhost'; }; const extractConnectionAttributeOrLog = ( @@ -94,7 +110,7 @@ const extractConnectionAttributeOrLog = ( }; export const getConnectionAttributesFromServer = ( - conn: amqp.Connection["connection"] + conn: amqp.Connection['connection'] ): SpanAttributes => { const product = conn.serverProperties.product?.toLowerCase?.(); if (product) { @@ -110,11 +126,11 @@ export const getConnectionAttributesFromUrl = ( url: string | amqp.Options.Connect ): SpanAttributes => { const attributes: SpanAttributes = { - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", // this is the only protocol supported by the instrumented library + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', // this is the only protocol supported by the instrumented library }; - url = url || "amqp://localhost"; - if (typeof url === "object") { + url = url || 'amqp://localhost'; + if (typeof url === 'object') { const connectOptions = url as amqp.Options.Connect; const protocol = getProtocol(connectOptions?.protocol); @@ -123,7 +139,7 @@ export const getConnectionAttributesFromUrl = ( url, SemanticAttributes.MESSAGING_PROTOCOL, protocol, - "protocol" + 'protocol' ), }); @@ -133,7 +149,7 @@ export const getConnectionAttributesFromUrl = ( url, SemanticAttributes.NET_PEER_NAME, hostname, - "hostname" + 'hostname' ), }); @@ -143,14 +159,14 @@ export const getConnectionAttributesFromUrl = ( url, SemanticAttributes.NET_PEER_PORT, port, - "port" + 'port' ), }); } else { const censoredUrl = censorPassword(url); attributes[SemanticAttributes.MESSAGING_URL] = censoredUrl; try { - const urlParts = new URL(censoredUrl); + const urlParts = urlLib.parse(censoredUrl); const protocol = getProtocol(urlParts.protocol); Object.assign(attributes, { @@ -158,7 +174,7 @@ export const getConnectionAttributesFromUrl = ( censoredUrl, SemanticAttributes.MESSAGING_PROTOCOL, protocol, - "protocol" + 'protocol' ), }); @@ -168,22 +184,25 @@ export const getConnectionAttributesFromUrl = ( censoredUrl, SemanticAttributes.NET_PEER_NAME, hostname, - "hostname" + 'hostname' ), }); - const port = getPort(parseInt(urlParts.port), protocol); + const port = getPort( + urlParts.port ? parseInt(urlParts.port) : undefined, + protocol + ); Object.assign(attributes, { ...extractConnectionAttributeOrLog( censoredUrl, SemanticAttributes.NET_PEER_PORT, port, - "port" + 'port' ), }); } catch (err) { diag.error( - "amqplib instrumentation: error while extracting connection details from connection url", + 'amqplib instrumentation: error while extracting connection details from connection url', { censoredUrl, err, diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts index aa88757508..317e95b6d9 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts @@ -1,237 +1,362 @@ +/* + * 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. + */ import 'mocha'; import * as expect from 'expect'; import { AmqplibInstrumentation } from '../src'; -import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; registerInstrumentationTesting(new AmqplibInstrumentation()); import * as amqpCallback from 'amqplib/callback_api'; -import { MessagingDestinationKindValues, SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { + MessagingDestinationKindValues, + SemanticAttributes, +} from '@opentelemetry/semantic-conventions'; import { context, SpanKind } from '@opentelemetry/api'; import { asyncConfirmSend, asyncConsume } from './utils'; -import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { + TEST_RABBITMQ_HOST, + TEST_RABBITMQ_PASS, + TEST_RABBITMQ_PORT, + TEST_RABBITMQ_USER, +} from './config'; const msgPayload = 'payload from test'; const queueName = 'queue-name-from-unittest'; -describe('amqplib instrumentation callback model', function () { - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - let conn: amqpCallback.Connection; - before((done) => { - amqpCallback.connect(url, (err, connection) => { - conn = connection; - done(); - }); - }); - after((done) => { - conn.close(() => done()); +describe('amqplib instrumentation callback model', () => { + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + let conn: amqpCallback.Connection; + before(done => { + amqpCallback.connect(url, (err, connection) => { + conn = connection; + done(); }); + }); + after(done => { + conn.close(() => done()); + }); - describe('channel', () => { - let channel: amqpCallback.Channel; - beforeEach((done) => { - conn.createChannel( - context.bind(context.active(), (err, c) => { - channel = c; - // install an error handler, otherwise when we have tests that create error on the channel, - // it throws and crash process - channel.on('error', () => {}); - channel.assertQueue( - queueName, - { durable: false }, - context.bind(context.active(), (err, ok) => { - channel.purgeQueue( - queueName, - context.bind(context.active(), (err, ok) => { - done(); - }) - ); - }) - ); + describe('channel', () => { + let channel: amqpCallback.Channel; + beforeEach(done => { + conn.createChannel( + context.bind(context.active(), (err, c) => { + channel = c; + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + channel.on('error', () => {}); + channel.assertQueue( + queueName, + { durable: false }, + context.bind(context.active(), (err, ok) => { + channel.purgeQueue( + queueName, + context.bind(context.active(), (err, ok) => { + done(); }) - ); - }); + ); + }) + ); + }) + ); + }); - afterEach((done) => { - try { - channel.close((err) => { - done(); - }); - } catch {} + afterEach(done => { + try { + channel.close(err => { + done(); }); + } catch {} + }); - it('simple publish and consume from queue callback', (done) => { - const hadSpaceInBuffer = channel.sendToQueue(queueName, Buffer.from(msgPayload)); - expect(hadSpaceInBuffer).toBeTruthy(); - - asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { - noAck: true, - }).then(() => { - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); - expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); - - done(); - }); - }); + it('simple publish and consume from queue callback', done => { + const hadSpaceInBuffer = channel.sendToQueue( + queueName, + Buffer.from(msgPayload) + ); + expect(hadSpaceInBuffer).toBeTruthy(); - it('end span with ack sync', (done) => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); + asyncConsume( + channel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ).then(() => { + const [publishSpan, consumeSpan] = getTestSpans(); - asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]).then(() => { - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - done(); - }); - }); + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toEqual(censoredUrl); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); - it('end span with ack async', (done) => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - asyncConsume(channel, queueName, [ - (msg) => - setTimeout(() => { - channel.ack(msg); - expect(getTestSpans().length).toBe(2); - done(); - }, 1), - ]); - }); + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toEqual(censoredUrl); + expect( + consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + expect(consumeSpan.parentSpanId).toEqual( + publishSpan.spanContext().spanId + ); + + done(); + }); + }); + + it('end span with ack sync', done => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + asyncConsume(channel, queueName, [msg => channel.ack(msg)]).then(() => { + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + done(); + }); }); - describe('confirm channel', () => { - let confirmChannel: amqpCallback.ConfirmChannel; - beforeEach((done) => { - conn.createConfirmChannel( - context.bind(context.active(), (err, c) => { - confirmChannel = c; - // install an error handler, otherwise when we have tests that create error on the channel, - // it throws and crash process - confirmChannel.on('error', () => {}); - confirmChannel.assertQueue( - queueName, - { durable: false }, - context.bind(context.active(), (err, ok) => { - confirmChannel.purgeQueue( - queueName, - context.bind(context.active(), (err, ok) => { - done(); - }) - ); - }) - ); + it('end span with ack async', done => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + asyncConsume(channel, queueName, [ + msg => + setTimeout(() => { + channel.ack(msg); + expect(getTestSpans().length).toBe(2); + done(); + }, 1), + ]); + }); + }); + + describe('confirm channel', () => { + let confirmChannel: amqpCallback.ConfirmChannel; + beforeEach(done => { + conn.createConfirmChannel( + context.bind(context.active(), (err, c) => { + confirmChannel = c; + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + confirmChannel.on('error', () => {}); + confirmChannel.assertQueue( + queueName, + { durable: false }, + context.bind(context.active(), (err, ok) => { + confirmChannel.purgeQueue( + queueName, + context.bind(context.active(), (err, ok) => { + done(); }) - ); - }); + ); + }) + ); + }) + ); + }); - afterEach((done) => { - try { - confirmChannel.close((err) => { - done(); - }); - } catch {} + afterEach(done => { + try { + confirmChannel.close(err => { + done(); }); + } catch {} + }); - it('simple publish and consume from queue callback', (done) => { - asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { - asyncConsume(confirmChannel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { - noAck: true, - }).then(() => { - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual( - queueName - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual( - queueName - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); - expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); - - done(); - }); - }); - }); + it('simple publish and consume from queue callback', done => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume( + confirmChannel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ).then(() => { + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_DESTINATION_KIND + ] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_PROTOCOL_VERSION + ] + ).toEqual('0.9.1'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toEqual(censoredUrl); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_DESTINATION_KIND + ] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_PROTOCOL_VERSION + ] + ).toEqual('0.9.1'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toEqual(censoredUrl); + expect( + consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + expect(consumeSpan.parentSpanId).toEqual( + publishSpan.spanContext().spanId + ); - it('end span with ack sync', (done) => { - asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { - asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.ack(msg)]).then(() => { - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - done(); - }); - }); + done(); }); + }); + }); - it('end span with ack async', (done) => { - asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { - asyncConsume(confirmChannel, queueName, [ - (msg) => - setTimeout(() => { - confirmChannel.ack(msg); - expect(getTestSpans().length).toBe(2); - done(); - }, 1), - ]); - }); + it('end span with ack sync', done => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume(confirmChannel, queueName, [ + msg => confirmChannel.ack(msg), + ]).then(() => { + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + done(); }); + }); + }); + + it('end span with ack async', done => { + asyncConfirmSend(confirmChannel, queueName, msgPayload).then(() => { + asyncConsume(confirmChannel, queueName, [ + msg => + setTimeout(() => { + confirmChannel.ack(msg); + expect(getTestSpans().length).toBe(2); + done(); + }, 1), + ]); + }); }); + }); }); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts index 6b78e9b375..27954039ab 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts @@ -1,105 +1,168 @@ +/* + * 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. + */ import 'mocha'; import * as expect from 'expect'; -import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { + TEST_RABBITMQ_HOST, + TEST_RABBITMQ_PASS, + TEST_RABBITMQ_PORT, + TEST_RABBITMQ_USER, +} from './config'; import { AmqplibInstrumentation } from '../src'; -import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; registerInstrumentationTesting(new AmqplibInstrumentation()); import * as amqp from 'amqplib'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -describe('amqplib instrumentation connection', function () { - describe('connect with url object', () => { - it('should extract connection attributes form url options', async function () { - const testName = this.test!.title; - const conn = await amqp.connect({ - protocol: 'amqp', - username: TEST_RABBITMQ_USER, - password: TEST_RABBITMQ_PASS, - hostname: TEST_RABBITMQ_HOST, - port: TEST_RABBITMQ_PORT, - }); +describe('amqplib instrumentation connection', () => { + describe('connect with url object', () => { + it('should extract connection attributes form url options', async function () { + const testName = this.test!.title; + const conn = await amqp.connect({ + protocol: 'amqp', + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + hostname: TEST_RABBITMQ_HOST, + port: TEST_RABBITMQ_PORT, + }); - try { - const channel = await conn.createChannel(); - channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); - const [publishSpan] = getTestSpans(); + try { + const channel = await conn.createChannel(); + channel.sendToQueue( + testName, + Buffer.from('message created only to test connection attributes') + ); + const [publishSpan] = getTestSpans(); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toBeUndefined(); // no url string if value supplied as object - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - } finally { - await conn.close(); - } - }); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toBeUndefined(); // no url string if value supplied as object + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); + } finally { + await conn.close(); + } + }); - it('should use default protocol', async function () { - const testName = this.test!.title; - const conn = await amqp.connect({ - username: TEST_RABBITMQ_USER, - password: TEST_RABBITMQ_PASS, - hostname: TEST_RABBITMQ_HOST, - port: TEST_RABBITMQ_PORT, - }); + it('should use default protocol', async function () { + const testName = this.test!.title; + const conn = await amqp.connect({ + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + hostname: TEST_RABBITMQ_HOST, + port: TEST_RABBITMQ_PORT, + }); - try { - const channel = await conn.createChannel(); - channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); - const [publishSpan] = getTestSpans(); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - } finally { - await conn.close(); - } - }); + try { + const channel = await conn.createChannel(); + channel.sendToQueue( + testName, + Buffer.from('message created only to test connection attributes') + ); + const [publishSpan] = getTestSpans(); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + } finally { + await conn.close(); + } + }); - it('should use default host', async function () { - if (TEST_RABBITMQ_HOST !== 'localhost') { - return; - } + it('should use default host', async function () { + if (TEST_RABBITMQ_HOST !== 'localhost') { + return; + } - const testName = this.test!.title; - const conn = await amqp.connect({ - protocol: 'amqp', - username: TEST_RABBITMQ_USER, - password: TEST_RABBITMQ_PASS, - port: TEST_RABBITMQ_PORT, - }); + const testName = this.test!.title; + const conn = await amqp.connect({ + protocol: 'amqp', + username: TEST_RABBITMQ_USER, + password: TEST_RABBITMQ_PASS, + port: TEST_RABBITMQ_PORT, + }); - try { - const channel = await conn.createChannel(); - channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); - const [publishSpan] = getTestSpans(); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - } finally { - await conn.close(); - } - }); + try { + const channel = await conn.createChannel(); + channel.sendToQueue( + testName, + Buffer.from('message created only to test connection attributes') + ); + const [publishSpan] = getTestSpans(); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + } finally { + await conn.close(); + } }); + }); - describe('connect with url string', () => { - it('should extract connection attributes from url options', async function () { - const testName = this.test!.title; - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const conn = await amqp.connect(url); + describe('connect with url string', () => { + it('should extract connection attributes from url options', async function () { + const testName = this.test!.title; + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const conn = await amqp.connect(url); - try { - const channel = await conn.createChannel(); - channel.sendToQueue(testName, Buffer.from('message created only to test connection attributes')); - const [publishSpan] = getTestSpans(); + try { + const channel = await conn.createChannel(); + channel.sendToQueue( + testName, + Buffer.from('message created only to test connection attributes') + ); + const [publishSpan] = getTestSpans(); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - } finally { - await conn.close(); - } - }); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_URL] + ).toEqual(censoredUrl); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_NAME] + ).toEqual(TEST_RABBITMQ_HOST); + expect( + publishSpan.attributes[SemanticAttributes.NET_PEER_PORT] + ).toEqual(TEST_RABBITMQ_PORT); + } finally { + await conn.close(); + } }); + }); }); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index f678bc4dea..2db1bf653f 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -1,18 +1,46 @@ +/* + * 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. + */ import 'mocha'; import * as expect from 'expect'; import * as sinon from 'sinon'; import * as lodash from 'lodash'; import { AmqplibInstrumentation, EndOperation, PublishParams } from '../src'; -import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; -const instrumentation = registerInstrumentationTesting(new AmqplibInstrumentation()); +const instrumentation = registerInstrumentationTesting( + new AmqplibInstrumentation() +); import * as amqp from 'amqplib'; import { ConsumeMessage } from 'amqplib'; -import { MessagingDestinationKindValues, SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { + MessagingDestinationKindValues, + SemanticAttributes, +} from '@opentelemetry/semantic-conventions'; import { Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from './utils'; -import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { + TEST_RABBITMQ_HOST, + TEST_RABBITMQ_PASS, + TEST_RABBITMQ_PORT, + TEST_RABBITMQ_USER, +} from './config'; import { SinonSpy } from 'sinon'; const msgPayload = 'payload from test'; @@ -20,917 +48,1251 @@ const queueName = 'queue-name-from-unittest'; // signal that the channel is closed in test, thus it should not be closed again in afterEach. // could not find a way to get this from amqplib directly. -const CHANNEL_CLOSED_IN_TEST = Symbol('opentelemetry.amqplib.unittest.channel_closed_in_test'); - -describe('amqplib instrumentation promise model', function () { - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - let conn: amqp.Connection; - before(async () => { - conn = await amqp.connect(url); +const CHANNEL_CLOSED_IN_TEST = Symbol( + 'opentelemetry.amqplib.unittest.channel_closed_in_test' +); + +describe('amqplib instrumentation promise model', () => { + const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; + let conn: amqp.Connection; + before(async () => { + conn = await amqp.connect(url); + }); + after(async () => { + await conn.close(); + }); + + let endHookSpy: SinonSpy; + const expectConsumeEndSpyStatus = ( + expectedEndOperations: EndOperation[] + ): void => { + expect(endHookSpy.callCount).toBe(expectedEndOperations.length); + expectedEndOperations.forEach( + (endOperation: EndOperation, index: number) => { + expect(endHookSpy.args[index][3]).toEqual(endOperation); + switch (endOperation) { + case EndOperation.AutoAck: + case EndOperation.Ack: + case EndOperation.AckAll: + expect(endHookSpy.args[index][2]).toBeFalsy(); + break; + + case EndOperation.Reject: + case EndOperation.Nack: + case EndOperation.NackAll: + case EndOperation.ChannelClosed: + case EndOperation.ChannelError: + expect(endHookSpy.args[index][2]).toBeTruthy(); + break; + } + } + ); + }; + + describe('channel', () => { + let channel: amqp.Channel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; + beforeEach(async () => { + endHookSpy = sinon.spy(); + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + }); + + channel = await conn.createChannel(); + await channel.assertQueue(queueName, { durable: false }); + await channel.purgeQueue(queueName); + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + channel.on('error', (err: Error) => {}); }); - after(async () => { - await conn.close(); + afterEach(async () => { + if (!channel[CHANNEL_CLOSED_IN_TEST]) { + try { + await new Promise(resolve => { + channel.on('close', resolve); + channel.close(); + }); + } catch {} + } }); - let endHookSpy: SinonSpy; - const expectConsumeEndSpyStatus = (expectedEndOperations: EndOperation[]): void => { - expect(endHookSpy.callCount).toBe(expectedEndOperations.length); - expectedEndOperations.forEach((endOperation: EndOperation, index: number) => { - expect(endHookSpy.args[index][3]).toEqual(endOperation); - switch (endOperation) { - case EndOperation.AutoAck: - case EndOperation.Ack: - case EndOperation.AckAll: - expect(endHookSpy.args[index][2]).toBeFalsy(); - break; - - case EndOperation.Reject: - case EndOperation.Nack: - case EndOperation.NackAll: - case EndOperation.ChannelClosed: - case EndOperation.ChannelError: - expect(endHookSpy.args[index][2]).toBeTruthy(); - break; - } - }); - }; + it('simple publish and consume from queue', async () => { + const hadSpaceInBuffer = channel.sendToQueue( + queueName, + Buffer.from(msgPayload) + ); + expect(hadSpaceInBuffer).toBeTruthy(); + + await asyncConsume( + channel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( + censoredUrl + ); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual( + TEST_RABBITMQ_HOST + ); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual( + TEST_RABBITMQ_PORT + ); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( + censoredUrl + ); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual( + TEST_RABBITMQ_HOST + ); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual( + TEST_RABBITMQ_PORT + ); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + expect(consumeSpan.parentSpanId).toEqual( + publishSpan.spanContext().spanId + ); + + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); - describe('channel', () => { - let channel: amqp.Channel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; - beforeEach(async () => { - endHookSpy = sinon.spy(); - instrumentation.setConfig({ - consumeEndHook: endHookSpy, - }); - - channel = await conn.createChannel(); - await channel.assertQueue(queueName, { durable: false }); - await channel.purgeQueue(queueName); - // install an error handler, otherwise when we have tests that create error on the channel, - // it throws and crash process - channel.on('error', (err: Error) => {}); + describe('ending consume spans', () => { + it('message acked sync', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [msg => channel.ack(msg)]); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message acked async', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + // start async timer and ack the message after the callback returns + await new Promise(resolve => { + asyncConsume(channel, queueName, [ + msg => + setTimeout(() => { + channel.ack(msg); + resolve(); + }, 1), + ]); }); - afterEach(async () => { - if (!channel[CHANNEL_CLOSED_IN_TEST]) { - try { - await new Promise((resolve) => { - channel.on('close', resolve); - channel.close(); - }); - } catch {} - } + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message nack no requeue', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [ + msg => channel.nack(msg, false, false), + ]); + await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + const [_, consumerSpan] = getTestSpans(); + expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(consumerSpan.status.message).toEqual( + 'nack called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Nack]); + }); + + it('message nack requeue, then acked', async () => { + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [ + (msg: amqp.Message) => channel.nack(msg, false, true), + (msg: amqp.Message) => channel.ack(msg), + ]); + // assert we have the requeued message sent again + expect(getTestSpans().length).toBe(3); + const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); + expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(rejectedConsumerSpan.status.message).toEqual( + 'nack called on message with requeue' + ); + expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); + }); + + it('ack allUpTo 2 msgs sync', async () => { + lodash.times(3, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [ + null, + msg => channel.ack(msg, true), + msg => channel.ack(msg), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.Ack, + EndOperation.Ack, + ]); + }); + + it('nack allUpTo 2 msgs sync', async () => { + lodash.times(3, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [ + null, + msg => channel.nack(msg, true, false), + msg => channel.nack(msg, false, false), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + lodash.range(3, 6).forEach(i => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual( + 'nack called on message without requeue' + ); }); - - it('simple publish and consume from queue', async () => { - const hadSpaceInBuffer = channel.sendToQueue(queueName, Buffer.from(msgPayload)); - expect(hadSpaceInBuffer).toBeTruthy(); - - await asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { - noAck: true, - }); - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); - expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); - - expectConsumeEndSpyStatus([EndOperation.AutoAck]); + expectConsumeEndSpyStatus([ + EndOperation.Nack, + EndOperation.Nack, + EndOperation.Nack, + ]); + }); + + it('ack not in received order', async () => { + lodash.times(3, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + const msgs = await asyncConsume(channel, queueName, [null, null, null]); + channel.ack(msgs[1]); + channel.ack(msgs[2]); + channel.ack(msgs[0]); + // assert all 3 span messages are ended + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.Ack, + EndOperation.Ack, + ]); + }); + + it('ackAll', async () => { + lodash.times(2, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [null, () => channel.ackAll()]); + // assert all 2 span messages are ended by call to ackAll + expect(getTestSpans().length).toBe(4); + expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); + }); + + it('nackAll', async () => { + lodash.times(2, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [ + null, + () => channel.nackAll(false), + ]); + // assert all 2 span messages are ended by calling nackAll + expect(getTestSpans().length).toBe(4); + lodash.range(2, 4).forEach(i => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual( + 'nackAll called on message without requeue' + ); + }); + expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); + }); + + it('reject', async () => { + lodash.times(1, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [ + msg => channel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual( + 'reject called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Reject]); + }); + + it('reject with requeue', async () => { + lodash.times(1, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [ + msg => channel.reject(msg, true), + msg => channel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(3); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual( + 'reject called on message with requeue' + ); + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[2].status.message).toEqual( + 'reject called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); + }); + + it('closing channel should end all open spans on it', async () => { + lodash.times(1, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await new Promise(resolve => + asyncConsume(channel, queueName, [ + async msg => { + await channel.close(); + resolve(); + channel[CHANNEL_CLOSED_IN_TEST] = true; + }, + ]) + ); + + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); + }); + + it('error on channel should end all open spans on it', done => { + lodash.times(2, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + channel.on('close', () => { + expect(getTestSpans().length).toBe(4); + // second consume ended with valid ack, previous message not acked when channel is errored. + // since we first ack the second message, it appear first in the finished spans array + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); + expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[3].status.message).toEqual('channel error'); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.ChannelError, + ]); + done(); }); + asyncConsume(channel, queueName, [ + null, + msg => { + try { + channel.ack(msg); + channel[CHANNEL_CLOSED_IN_TEST] = true; + // ack the same msg again, this is not valid and should close the channel + channel.ack(msg); + } catch {} + }, + ]); + }); + + it('not acking the message trigger timeout', async () => { + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + consumeTimeoutMs: 1, + }); + + lodash.times(1, () => + channel.sendToQueue(queueName, Buffer.from(msgPayload)) + ); + + await asyncConsume(channel, queueName, [null]); + + // we have timeout of 1 ms, so we wait more than that and check span indeed ended + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); + }); + }); - describe('ending consume spans', () => { - it('message acked sync', async () => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - await asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]); - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.Ack]); - }); - - it('message acked async', async () => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - // start async timer and ack the message after the callback returns - await new Promise((resolve) => { - asyncConsume(channel, queueName, [ - (msg) => - setTimeout(() => { - channel.ack(msg); - resolve(); - }, 1), - ]); - }); - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.Ack]); - }); - - it('message nack no requeue', async () => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - await asyncConsume(channel, queueName, [(msg) => channel.nack(msg, false, false)]); - await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - const [_, consumerSpan] = getTestSpans(); - expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); - expect(consumerSpan.status.message).toEqual('nack called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Nack]); - }); - - it('message nack requeue, then acked', async () => { - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - // @ts-ignore - await asyncConsume(channel, queueName, [ - (msg: amqp.Message) => channel.nack(msg, false, true), - (msg: amqp.Message) => channel.ack(msg), - ]); - // assert we have the requeued message sent again - expect(getTestSpans().length).toBe(3); - const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); - expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); - expect(rejectedConsumerSpan.status.message).toEqual('nack called on message with requeue'); - expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); - expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); - }); - - it('ack allUpTo 2 msgs sync', async () => { - lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [ - null, - (msg) => channel.ack(msg, true), - (msg) => channel.ack(msg), - ]); - // assert all 3 messages are acked, including the first one which is acked by allUpTo - expect(getTestSpans().length).toBe(6); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); - }); - - it('nack allUpTo 2 msgs sync', async () => { - lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [ - null, - (msg) => channel.nack(msg, true, false), - (msg) => channel.nack(msg, false, false), - ]); - // assert all 3 messages are acked, including the first one which is acked by allUpTo - expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach((i) => { - expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[i].status.message).toEqual('nack called on message without requeue'); - }); - expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Nack, EndOperation.Nack]); - }); - - it('ack not in received order', async () => { - lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - const msgs = await asyncConsume(channel, queueName, [null, null, null]); - channel.ack(msgs[1]); - channel.ack(msgs[2]); - channel.ack(msgs[0]); - // assert all 3 span messages are ended - expect(getTestSpans().length).toBe(6); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); - }); - - it('ackAll', async () => { - lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [null, () => channel.ackAll()]); - // assert all 2 span messages are ended by call to ackAll - expect(getTestSpans().length).toBe(4); - expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); - }); - - it('nackAll', async () => { - lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [null, () => channel.nackAll(false)]); - // assert all 2 span messages are ended by calling nackAll - expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach((i) => { - expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[i].status.message).toEqual('nackAll called on message without requeue'); - }); - expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); - }); - - it('reject', async () => { - lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [(msg) => channel.reject(msg, false)]); - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('reject called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Reject]); - }); - - it('reject with requeue', async () => { - lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - // @ts-ignore - await asyncConsume(channel, queueName, [ - (msg) => channel.reject(msg, true), - (msg) => channel.reject(msg, false), - ]); - expect(getTestSpans().length).toBe(3); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('reject called on message with requeue'); - expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[2].status.message).toEqual('reject called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); - }); - - it('closing channel should end all open spans on it', async () => { - lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - await new Promise((resolve) => - asyncConsume(channel, queueName, [ - async (msg) => { - await channel.close(); - resolve(); - channel[CHANNEL_CLOSED_IN_TEST] = true; - }, - ]) - ); - - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('channel closed'); - expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); - }); - - it('error on channel should end all open spans on it', (done) => { - lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - channel.on('close', () => { - expect(getTestSpans().length).toBe(4); - // second consume ended with valid ack, previous message not acked when channel is errored. - // since we first ack the second message, it appear first in the finished spans array - expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); - expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual('channel error'); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.ChannelError]); - done(); - }); - asyncConsume(channel, queueName, [ - null, - (msg) => { - try { - channel.ack(msg); - channel[CHANNEL_CLOSED_IN_TEST] = true; - // ack the same msg again, this is not valid and should close the channel - channel.ack(msg); - } catch {} - }, - ]); - }); - - it('not acking the message trigger timeout', async () => { - instrumentation.setConfig({ - consumeEndHook: endHookSpy, - consumeTimeoutMs: 1, - }); - - lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload))); - - await asyncConsume(channel, queueName, [null]); - - // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); - }); + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await channel.assertExchange(exchangeName, 'topic', { durable: false }); + + const { queue: queueName } = await channel.assertQueue('', { + durable: false, }); + await channel.bindQueue(queueName, exchangeName, '#'); + + channel.publish(exchangeName, routingKey, Buffer.from(msgPayload)); - describe('routing and exchange', () => { - it('topic exchange', async () => { - const exchangeName = 'topic exchange'; - const routingKey = 'topic.name.from.unittest'; - await channel.assertExchange(exchangeName, 'topic', { durable: false }); - - const { queue: queueName } = await channel.assertQueue('', { durable: false }); - await channel.bindQueue(queueName, exchangeName, '#'); - - channel.publish(exchangeName, routingKey, Buffer.from(msgPayload)); - - await asyncConsume(channel, queueName, [null], { - noAck: true, - }); - - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); - expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); - }); + await asyncConsume(channel, queueName, [null], { + noAck: true, }); - it('moduleVersionAttributeName works with publish and consume', async () => { - const VERSION_ATTR = 'module.version'; - instrumentation.setConfig({ - moduleVersionAttributeName: VERSION_ATTR, - }); + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(exchangeName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(routingKey); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(exchangeName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(routingKey); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + expect(consumeSpan.parentSpanId).toEqual( + publishSpan.spanContext().spanId + ); + }); + }); - channel.sendToQueue(queueName, Buffer.from(msgPayload)); + it('moduleVersionAttributeName works with publish and consume', async () => { + const VERSION_ATTR = 'module.version'; + instrumentation.setConfig({ + moduleVersionAttributeName: VERSION_ATTR, + }); + + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume( + channel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach(s => + expect(s.attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ) + ); + }); - await asyncConsume(channel, queueName, [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { - noAck: true, - }); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => expect(s.attributes[VERSION_ATTR]).toMatch(/\d{1,4}\.\d{1,4}\.\d{1,5}.*/)); + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + const attributeNameFromEndHook = 'attribute.name.from.endhook'; + const endHookAttributeValue = 'attribute value from end hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(msg!.content.toString()).toEqual(msgPayload); + }, + consumeEndHook: ( + span: Span, + msg: amqp.ConsumeMessage | null, + rejected: boolean | null, + endOperation: EndOperation + ): void => { + span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); + expect(endOperation).toEqual(EndOperation.AutoAck); + }, }); - describe('hooks', () => { - it('publish and consume hooks success', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; - const attributeNameFromEndHook = 'attribute.name.from.endhook'; - const endHookAttributeValue = 'attribute value from end hook'; - instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(publishParams.exchange).toEqual(''); - expect(publishParams.routingKey).toEqual(queueName); - expect(publishParams.content.toString()).toEqual(msgPayload); - }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(msg!.content.toString()).toEqual(msgPayload); - }, - consumeEndHook: ( - span: Span, - msg: amqp.ConsumeMessage | null, - rejected: boolean | null, - endOperation: EndOperation - ): void => { - span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); - expect(endOperation).toEqual(EndOperation.AutoAck); - }, - }); - - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - await asyncConsume(channel, queueName, [null], { - noAck: true, - }); - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); - expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); - expect(getTestSpans()[1].attributes[attributeNameFromEndHook]).toEqual(endHookAttributeValue); - }); - - it('hooks throw should not affect user flow or span creation', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; - instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); - }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); - }, - }); - - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - await asyncConsume(channel, queueName, [null], { - noAck: true, - }); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual(hookAttributeValue)); - }); + channel.sendToQueue(queueName, Buffer.from(msgPayload)); + + await asyncConsume(channel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ); + expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ); + expect(getTestSpans()[1].attributes[attributeNameFromEndHook]).toEqual( + endHookAttributeValue + ); + }); + + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, }); - describe('delete queue', () => { - it('consumer receives null msg when a queue is deleted in broker', async () => { - const queueNameForDeletion = 'queue-to-be-deleted'; - await channel.assertQueue(queueNameForDeletion, { durable: false }); - await channel.purgeQueue(queueNameForDeletion); + channel.sendToQueue(queueName, Buffer.from(msgPayload)); - await channel.consume(queueNameForDeletion, (msg: ConsumeMessage | null) => {}, { noAck: true }); - await channel.deleteQueue(queueNameForDeletion); - }); + await asyncConsume(channel, queueName, [null], { + noAck: true, }); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach(s => + expect(s.attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ) + ); + }); + }); + + describe('delete queue', () => { + it('consumer receives null msg when a queue is deleted in broker', async () => { + const queueNameForDeletion = 'queue-to-be-deleted'; + await channel.assertQueue(queueNameForDeletion, { durable: false }); + await channel.purgeQueue(queueNameForDeletion); + + await channel.consume( + queueNameForDeletion, + (msg: ConsumeMessage | null) => {}, + { noAck: true } + ); + await channel.deleteQueue(queueNameForDeletion); + }); + }); + }); + + describe('confirm channel', () => { + let confirmChannel: amqp.ConfirmChannel & { + [CHANNEL_CLOSED_IN_TEST]?: boolean; + }; + beforeEach(async () => { + endHookSpy = sinon.spy(); + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + }); + + confirmChannel = await conn.createConfirmChannel(); + await confirmChannel.assertQueue(queueName, { durable: false }); + await confirmChannel.purgeQueue(queueName); + // install an error handler, otherwise when we have tests that create error on the channel, + // it throws and crash process + confirmChannel.on('error', (err: Error) => {}); + }); + afterEach(async () => { + if (!confirmChannel[CHANNEL_CLOSED_IN_TEST]) { + try { + await new Promise(resolve => { + confirmChannel.on('close', resolve); + confirmChannel.close(); + }); + } catch {} + } }); - describe('confirm channel', () => { - let confirmChannel: amqp.ConfirmChannel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; - beforeEach(async () => { - endHookSpy = sinon.spy(); - instrumentation.setConfig({ - consumeEndHook: endHookSpy, - }); - - confirmChannel = await conn.createConfirmChannel(); - await confirmChannel.assertQueue(queueName, { durable: false }); - await confirmChannel.purgeQueue(queueName); - // install an error handler, otherwise when we have tests that create error on the channel, - // it throws and crash process - confirmChannel.on('error', (err: Error) => {}); + it('simple publish with confirm and consume from queue', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume( + confirmChannel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( + censoredUrl + ); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual( + TEST_RABBITMQ_HOST + ); + expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual( + TEST_RABBITMQ_PORT + ); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(queueName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( + censoredUrl + ); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual( + TEST_RABBITMQ_HOST + ); + expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual( + TEST_RABBITMQ_PORT + ); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); + + it('confirm throw should not affect span end', async () => { + const confirmUserError = new Error('callback error'); + await asyncConfirmSend(confirmChannel, queueName, msgPayload, () => { + throw confirmUserError; + }).catch(reject => expect(reject).toEqual(confirmUserError)); + + await asyncConsume( + confirmChannel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + + expect(getTestSpans()).toHaveLength(2); + expectConsumeEndSpyStatus([EndOperation.AutoAck]); + }); + + describe('ending consume spans', () => { + it('message acked sync', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [ + msg => confirmChannel.ack(msg), + ]); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message acked async', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + // start async timer and ack the message after the callback returns + await new Promise(resolve => { + asyncConsume(confirmChannel, queueName, [ + msg => + setTimeout(() => { + confirmChannel.ack(msg); + resolve(); + }, 1), + ]); + }); + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.Ack]); + }); + + it('message nack no requeue', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [ + msg => confirmChannel.nack(msg, false, false), + ]); + await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again + // assert consumed message span has ended + expect(getTestSpans().length).toBe(2); + const [_, consumerSpan] = getTestSpans(); + expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(consumerSpan.status.message).toEqual( + 'nack called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Nack]); + }); + + it('message nack requeue, then acked', async () => { + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [ + (msg: amqp.Message) => confirmChannel.nack(msg, false, true), + (msg: amqp.Message) => confirmChannel.ack(msg), + ]); + // assert we have the requeued message sent again + expect(getTestSpans().length).toBe(3); + const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); + expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); + expect(rejectedConsumerSpan.status.message).toEqual( + 'nack called on message with requeue' + ); + expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); + expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); + }); + + it('ack allUpTo 2 msgs sync', async () => { + await Promise.all( + lodash.times(3, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + null, + msg => confirmChannel.ack(msg, true), + msg => confirmChannel.ack(msg), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.Ack, + EndOperation.Ack, + ]); + }); + + it('nack allUpTo 2 msgs sync', async () => { + await Promise.all( + lodash.times(3, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + null, + msg => confirmChannel.nack(msg, true, false), + msg => confirmChannel.nack(msg, false, false), + ]); + // assert all 3 messages are acked, including the first one which is acked by allUpTo + expect(getTestSpans().length).toBe(6); + lodash.range(3, 6).forEach(i => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual( + 'nack called on message without requeue' + ); + }); + expectConsumeEndSpyStatus([ + EndOperation.Nack, + EndOperation.Nack, + EndOperation.Nack, + ]); + }); + + it('ack not in received order', async () => { + await Promise.all( + lodash.times(3, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + const msgs = await asyncConsume(confirmChannel, queueName, [ + null, + null, + null, + ]); + confirmChannel.ack(msgs[1]); + confirmChannel.ack(msgs[2]); + confirmChannel.ack(msgs[0]); + // assert all 3 span messages are ended + expect(getTestSpans().length).toBe(6); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.Ack, + EndOperation.Ack, + ]); + }); + + it('ackAll', async () => { + await Promise.all( + lodash.times(2, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + null, + () => confirmChannel.ackAll(), + ]); + // assert all 2 span messages are ended by call to ackAll + expect(getTestSpans().length).toBe(4); + expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); + }); + + it('nackAll', async () => { + await Promise.all( + lodash.times(2, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + null, + () => confirmChannel.nackAll(false), + ]); + // assert all 2 span messages are ended by calling nackAll + expect(getTestSpans().length).toBe(4); + lodash.range(2, 4).forEach(i => { + expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[i].status.message).toEqual( + 'nackAll called on message without requeue' + ); }); - afterEach(async () => { - if (!confirmChannel[CHANNEL_CLOSED_IN_TEST]) { - try { - await new Promise((resolve) => { - confirmChannel.on('close', resolve); - confirmChannel.close(); - }); - } catch {} - } + expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); + }); + + it('reject', async () => { + await Promise.all( + lodash.times(1, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + msg => confirmChannel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual( + 'reject called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Reject]); + }); + + it('reject with requeue', async () => { + await Promise.all( + lodash.times(1, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await asyncConsume(confirmChannel, queueName, [ + msg => confirmChannel.reject(msg, true), + msg => confirmChannel.reject(msg, false), + ]); + expect(getTestSpans().length).toBe(3); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual( + 'reject called on message with requeue' + ); + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[2].status.message).toEqual( + 'reject called on message without requeue' + ); + expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); + }); + + it('closing channel should end all open spans on it', async () => { + await Promise.all( + lodash.times(1, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); + + await new Promise(resolve => + asyncConsume(confirmChannel, queueName, [ + async msg => { + await confirmChannel.close(); + resolve(); + confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; + }, + ]) + ); + + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); + }); + + it('error on channel should end all open spans on it', done => { + Promise.all( + lodash.times(2, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ).then(() => { + confirmChannel.on('close', () => { + expect(getTestSpans().length).toBe(4); + // second consume ended with valid ack, previous message not acked when channel is errored. + // since we first ack the second message, it appear first in the finished spans array + expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); + expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); + expect(getTestSpans()[3].status.message).toEqual('channel error'); + expectConsumeEndSpyStatus([ + EndOperation.Ack, + EndOperation.ChannelError, + ]); + done(); + }); + asyncConsume(confirmChannel, queueName, [ + null, + msg => { + try { + confirmChannel.ack(msg); + confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; + // ack the same msg again, this is not valid and should close the channel + confirmChannel.ack(msg); + } catch {} + }, + ]); }); + }); - it('simple publish with confirm and consume from queue', async () => { - await asyncConfirmSend(confirmChannel, queueName, msgPayload); + it('not acking the message trigger timeout', async () => { + instrumentation.setConfig({ + consumeEndHook: endHookSpy, + consumeTimeoutMs: 1, + }); - await asyncConsume( - confirmChannel, - queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], - { - noAck: true, - } - ); - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(publishSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(queueName); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual(censoredUrl); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(TEST_RABBITMQ_HOST); - expect(consumeSpan.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(TEST_RABBITMQ_PORT); + await Promise.all( + lodash.times(1, () => + asyncConfirmSend(confirmChannel, queueName, msgPayload) + ) + ); - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); + await asyncConsume(confirmChannel, queueName, [null]); - expectConsumeEndSpyStatus([EndOperation.AutoAck]); - }); + // we have timeout of 1 ms, so we wait more than that and check span indeed ended + await new Promise(resolve => setTimeout(resolve, 10)); - it('confirm throw should not affect span end', async () => { - const confirmUserError = new Error('callback error'); - await asyncConfirmSend(confirmChannel, queueName, msgPayload, () => { - throw confirmUserError; - }).catch((reject) => expect(reject).toEqual(confirmUserError)); - - await asyncConsume( - confirmChannel, - queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], - { - noAck: true, - } - ); + expect(getTestSpans().length).toBe(2); + expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); + }); + }); - expect(getTestSpans()).toHaveLength(2); - expectConsumeEndSpyStatus([EndOperation.AutoAck]); + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await confirmChannel.assertExchange(exchangeName, 'topic', { + durable: false, }); - describe('ending consume spans', () => { - it('message acked sync', async () => { - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.ack(msg)]); - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.Ack]); - }); - - it('message acked async', async () => { - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - // start async timer and ack the message after the callback returns - await new Promise((resolve) => { - asyncConsume(confirmChannel, queueName, [ - (msg) => - setTimeout(() => { - confirmChannel.ack(msg); - resolve(); - }, 1), - ]); - }); - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.Ack]); - }); - - it('message nack no requeue', async () => { - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.nack(msg, false, false)]); - await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again - // assert consumed message span has ended - expect(getTestSpans().length).toBe(2); - const [_, consumerSpan] = getTestSpans(); - expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); - expect(consumerSpan.status.message).toEqual('nack called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Nack]); - }); - - it('message nack requeue, then acked', async () => { - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [ - (msg: amqp.Message) => confirmChannel.nack(msg, false, true), - (msg: amqp.Message) => confirmChannel.ack(msg), - ]); - // assert we have the requeued message sent again - expect(getTestSpans().length).toBe(3); - const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); - expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); - expect(rejectedConsumerSpan.status.message).toEqual('nack called on message with requeue'); - expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); - expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); - }); - - it('ack allUpTo 2 msgs sync', async () => { - await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [ - null, - (msg) => confirmChannel.ack(msg, true), - (msg) => confirmChannel.ack(msg), - ]); - // assert all 3 messages are acked, including the first one which is acked by allUpTo - expect(getTestSpans().length).toBe(6); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); - }); - - it('nack allUpTo 2 msgs sync', async () => { - await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [ - null, - (msg) => confirmChannel.nack(msg, true, false), - (msg) => confirmChannel.nack(msg, false, false), - ]); - // assert all 3 messages are acked, including the first one which is acked by allUpTo - expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach((i) => { - expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[i].status.message).toEqual('nack called on message without requeue'); - }); - expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Nack, EndOperation.Nack]); - }); - - it('ack not in received order', async () => { - await Promise.all(lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - const msgs = await asyncConsume(confirmChannel, queueName, [null, null, null]); - confirmChannel.ack(msgs[1]); - confirmChannel.ack(msgs[2]); - confirmChannel.ack(msgs[0]); - // assert all 3 span messages are ended - expect(getTestSpans().length).toBe(6); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.Ack, EndOperation.Ack]); - }); - - it('ackAll', async () => { - await Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [null, () => confirmChannel.ackAll()]); - // assert all 2 span messages are ended by call to ackAll - expect(getTestSpans().length).toBe(4); - expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); - }); - - it('nackAll', async () => { - await Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [null, () => confirmChannel.nackAll(false)]); - // assert all 2 span messages are ended by calling nackAll - expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach((i) => { - expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[i].status.message).toEqual('nackAll called on message without requeue'); - }); - expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); - }); - - it('reject', async () => { - await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [(msg) => confirmChannel.reject(msg, false)]); - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('reject called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Reject]); - }); - - it('reject with requeue', async () => { - await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - // @ts-ignore - await asyncConsume(confirmChannel, queueName, [ - (msg) => confirmChannel.reject(msg, true), - (msg) => confirmChannel.reject(msg, false), - ]); - expect(getTestSpans().length).toBe(3); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('reject called on message with requeue'); - expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[2].status.message).toEqual('reject called on message without requeue'); - expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); - }); - - it('closing channel should end all open spans on it', async () => { - await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - await new Promise((resolve) => - asyncConsume(confirmChannel, queueName, [ - async (msg) => { - await confirmChannel.close(); - resolve(); - confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; - }, - ]) - ); - - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('channel closed'); - expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); - }); - - it('error on channel should end all open spans on it', (done) => { - Promise.all(lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))).then(() => { - confirmChannel.on('close', () => { - expect(getTestSpans().length).toBe(4); - // second consume ended with valid ack, previous message not acked when channel is errored. - // since we first ack the second message, it appear first in the finished spans array - expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); - expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual('channel error'); - expectConsumeEndSpyStatus([EndOperation.Ack, EndOperation.ChannelError]); - done(); - }); - asyncConsume(confirmChannel, queueName, [ - null, - (msg) => { - try { - confirmChannel.ack(msg); - confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; - // ack the same msg again, this is not valid and should close the channel - confirmChannel.ack(msg); - } catch {} - }, - ]); - }); - }); - - it('not acking the message trigger timeout', async () => { - instrumentation.setConfig({ - consumeEndHook: endHookSpy, - consumeTimeoutMs: 1, - }); - - await Promise.all(lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload))); - - await asyncConsume(confirmChannel, queueName, [null]); - - // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(getTestSpans().length).toBe(2); - expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); - }); + const { queue: queueName } = await confirmChannel.assertQueue('', { + durable: false, }); + await confirmChannel.bindQueue(queueName, exchangeName, '#'); + + await asyncConfirmPublish( + confirmChannel, + exchangeName, + routingKey, + msgPayload + ); - describe('routing and exchange', () => { - it('topic exchange', async () => { - const exchangeName = 'topic exchange'; - const routingKey = 'topic.name.from.unittest'; - await confirmChannel.assertExchange(exchangeName, 'topic', { durable: false }); - - const { queue: queueName } = await confirmChannel.assertQueue('', { durable: false }); - await confirmChannel.bindQueue(queueName, exchangeName, '#'); - - await asyncConfirmPublish(confirmChannel, exchangeName, routingKey, msgPayload); - - await asyncConsume(confirmChannel, queueName, [null], { - noAck: true, - }); - - const [publishSpan, consumeSpan] = getTestSpans(); - - // assert publish span - expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - - // assert consume span - expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM]).toEqual('rabbitmq'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION]).toEqual(exchangeName); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND]).toEqual( - MessagingDestinationKindValues.TOPIC - ); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY]).toEqual(routingKey); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL]).toEqual('AMQP'); - expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION]).toEqual('0.9.1'); - - // assert context propagation - expect(consumeSpan.spanContext().traceId).toEqual(publishSpan.spanContext().traceId); - expect(consumeSpan.parentSpanId).toEqual(publishSpan.spanContext().spanId); - }); + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, }); - it('moduleVersionAttributeName works with publish and consume', async () => { - const VERSION_ATTR = 'module.version'; - instrumentation.setConfig({ - moduleVersionAttributeName: VERSION_ATTR, - }); - - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume( - confirmChannel, - queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], - { - noAck: true, - } + const [publishSpan, consumeSpan] = getTestSpans(); + + // assert publish span + expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(exchangeName); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + publishSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(routingKey); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + + // assert consume span + expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] + ).toEqual('rabbitmq'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] + ).toEqual(exchangeName); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] + ).toEqual(MessagingDestinationKindValues.TOPIC); + expect( + consumeSpan.attributes[ + SemanticAttributes.MESSAGING_RABBITMQ_ROUTING_KEY + ] + ).toEqual(routingKey); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] + ).toEqual('AMQP'); + expect( + consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] + ).toEqual('0.9.1'); + + // assert context propagation + expect(consumeSpan.spanContext().traceId).toEqual( + publishSpan.spanContext().traceId + ); + expect(consumeSpan.parentSpanId).toEqual( + publishSpan.spanContext().spanId + ); + }); + }); + + it('moduleVersionAttributeName works with publish and consume', async () => { + const VERSION_ATTR = 'module.version'; + instrumentation.setConfig({ + moduleVersionAttributeName: VERSION_ATTR, + }); + + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume( + confirmChannel, + queueName, + [msg => expect(msg.content.toString()).toEqual(msgPayload)], + { + noAck: true, + } + ); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach(s => + expect(s.attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ) + ); + }); + + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + const attributeNameFromConfirmEndHook = + 'attribute.name.from.confirm.endhook'; + const confirmEndHookAttributeValue = + 'attribute value from confirm end hook'; + const attributeNameFromConsumeEndHook = + 'attribute.name.from.consume.endhook'; + const consumeEndHookAttributeValue = + 'attribute value from consume end hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams) => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + expect(publishParams.isConfirmChannel).toBe(true); + }, + publishConfirmHook: (span, publishParams) => { + span.setAttribute( + attributeNameFromConfirmEndHook, + confirmEndHookAttributeValue ); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => expect(s.attributes[VERSION_ATTR]).toMatch(/\d{1,4}\.\d{1,4}\.\d{1,5}.*/)); + expect(publishParams.exchange).toEqual(''); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null) => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + expect(msg!.content.toString()).toEqual(msgPayload); + }, + consumeEndHook: ( + span: Span, + msg: amqp.ConsumeMessage | null, + rejected: boolean | null, + endOperation: EndOperation + ): void => { + span.setAttribute( + attributeNameFromConsumeEndHook, + consumeEndHookAttributeValue + ); + expect(endOperation).toEqual(EndOperation.AutoAck); + }, + }); + + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, + }); + expect(getTestSpans().length).toBe(2); + expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ); + expect( + getTestSpans()[0].attributes[attributeNameFromConfirmEndHook] + ).toEqual(confirmEndHookAttributeValue); + expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ); + expect( + getTestSpans()[1].attributes[attributeNameFromConsumeEndHook] + ).toEqual(consumeEndHookAttributeValue); + }); + + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + instrumentation.setConfig({ + publishHook: (span: Span, publishParams: PublishParams): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + publishConfirmHook: ( + span: Span, + publishParams: PublishParams + ): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, + consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + span.setAttribute(attributeNameFromHook, hookAttributeValue); + throw new Error('error from hook'); + }, }); - describe('hooks', () => { - it('publish and consume hooks success', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; - const attributeNameFromConfirmEndHook = 'attribute.name.from.confirm.endhook'; - const confirmEndHookAttributeValue = 'attribute value from confirm end hook'; - const attributeNameFromConsumeEndHook = 'attribute.name.from.consume.endhook'; - const consumeEndHookAttributeValue = 'attribute value from consume end hook'; - instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams) => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(publishParams.exchange).toEqual(''); - expect(publishParams.routingKey).toEqual(queueName); - expect(publishParams.content.toString()).toEqual(msgPayload); - expect(publishParams.isConfirmChannel).toBe(true); - }, - publishConfirmHook: (span, publishParams) => { - span.setAttribute(attributeNameFromConfirmEndHook, confirmEndHookAttributeValue); - expect(publishParams.exchange).toEqual(''); - expect(publishParams.routingKey).toEqual(queueName); - expect(publishParams.content.toString()).toEqual(msgPayload); - }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null) => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(msg!.content.toString()).toEqual(msgPayload); - }, - consumeEndHook: ( - span: Span, - msg: amqp.ConsumeMessage | null, - rejected: boolean | null, - endOperation: EndOperation - ): void => { - span.setAttribute(attributeNameFromConsumeEndHook, consumeEndHookAttributeValue); - expect(endOperation).toEqual(EndOperation.AutoAck); - }, - }); - - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume(confirmChannel, queueName, [null], { - noAck: true, - }); - expect(getTestSpans().length).toBe(2); - expect(getTestSpans()[0].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); - expect(getTestSpans()[0].attributes[attributeNameFromConfirmEndHook]).toEqual( - confirmEndHookAttributeValue - ); - expect(getTestSpans()[1].attributes[attributeNameFromHook]).toEqual(hookAttributeValue); - expect(getTestSpans()[1].attributes[attributeNameFromConsumeEndHook]).toEqual( - consumeEndHookAttributeValue - ); - }); - - it('hooks throw should not affect user flow or span creation', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; - instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); - }, - publishConfirmHook: (span: Span, publishParams: PublishParams): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); - }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); - }, - }); - - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume(confirmChannel, queueName, [null], { - noAck: true, - }); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual(hookAttributeValue)); - }); + await asyncConfirmSend(confirmChannel, queueName, msgPayload); + + await asyncConsume(confirmChannel, queueName, [null], { + noAck: true, }); + expect(getTestSpans().length).toBe(2); + getTestSpans().forEach(s => + expect(s.attributes[attributeNameFromHook]).toEqual( + hookAttributeValue + ) + ); + }); }); + }); }); diff --git a/plugins/node/instrumentation-amqplib/test/config.ts b/plugins/node/instrumentation-amqplib/test/config.ts index 2429e8fc81..98c9cadd4c 100644 --- a/plugins/node/instrumentation-amqplib/test/config.ts +++ b/plugins/node/instrumentation-amqplib/test/config.ts @@ -1,6 +1,18 @@ -// should match the values used to start the docker for tests: -// 1. 'test:docker:run' script in package.json -// 2. services in github actions workflow +/* + * 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. + */ export const TEST_RABBITMQ_HOST = 'localhost'; export const TEST_RABBITMQ_PORT = 22221; export const TEST_RABBITMQ_USER = 'username'; diff --git a/plugins/node/instrumentation-amqplib/test/utils.test.ts b/plugins/node/instrumentation-amqplib/test/utils.test.ts index cd44e53268..d8286d640f 100644 --- a/plugins/node/instrumentation-amqplib/test/utils.test.ts +++ b/plugins/node/instrumentation-amqplib/test/utils.test.ts @@ -1,151 +1,169 @@ -import "mocha"; -import * as expect from "expect"; -import { getConnectionAttributesFromServer, getConnectionAttributesFromUrl } from "../src/utils"; -import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; -import * as amqp from "amqplib"; -import { rabbitMqUrl } from "./utils"; - -describe("utils", function () { - describe("getConnectionAttributesFromServer", function () { +/* + * 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. + */ +import 'mocha'; +import * as expect from 'expect'; +import { + getConnectionAttributesFromServer, + getConnectionAttributesFromUrl, +} from '../src/utils'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import * as amqp from 'amqplib'; +import { rabbitMqUrl } from './utils'; +describe('utils', () => { + describe('getConnectionAttributesFromServer', () => { let conn: amqp.Connection; before(async () => { - conn = await amqp.connect(rabbitMqUrl); + conn = await amqp.connect(rabbitMqUrl); }); after(async () => { - await conn.close(); + await conn.close(); }); - it("messaging system attribute", function () { + it('messaging system attribute', () => { const attributes = getConnectionAttributesFromServer(conn.connection); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_SYSTEM]: 'rabbitmq', + [SemanticAttributes.MESSAGING_SYSTEM]: 'rabbitmq', }); }); }); - describe("getConnectionAttributesFromUrl", function () { - it("all features", function () { + describe('getConnectionAttributesFromUrl', () => { + it('all features', () => { const attributes = getConnectionAttributesFromUrl( - `amqp://user:pass@host:10000/vhost` + 'amqp://user:pass@host:10000/vhost' ); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'host', [SemanticAttributes.NET_PEER_PORT]: 10000, - [SemanticAttributes.MESSAGING_URL]: `amqp://user:***@host:10000/vhost`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://user:***@host:10000/vhost', }); }); - it("all features encoded", function () { + it('all features encoded', () => { const attributes = getConnectionAttributesFromUrl( - `amqp://user%61:%61pass@ho%61st:10000/v%2fhost` + 'amqp://user%61:%61pass@ho%61st:10000/v%2fhost' ); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "ho%61st", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'ho%61st', [SemanticAttributes.NET_PEER_PORT]: 10000, - [SemanticAttributes.MESSAGING_URL]: `amqp://user%61:***@ho%61st:10000/v%2fhost`, + [SemanticAttributes.MESSAGING_URL]: + 'amqp://user%61:***@ho%61st:10000/v%2fhost', }); }); - it("only protocol", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://`); + it('only protocol', () => { + const attributes = getConnectionAttributesFromUrl('amqp://'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "localhost", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'localhost', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp://`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://', }); }); - it("empty username and password", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://:@/`); + it('empty username and password', () => { + const attributes = getConnectionAttributesFromUrl('amqp://:@/'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.MESSAGING_URL]: `amqp://:***@/`, + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.MESSAGING_URL]: 'amqp://:***@/', }); }); - it("username and no password", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://user@`); + it('username and no password', () => { + const attributes = getConnectionAttributesFromUrl('amqp://user@'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.MESSAGING_URL]: `amqp://user@`, + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.MESSAGING_URL]: 'amqp://user@', }); }); - it("username and password, no host", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://user:pass@`); + it('username and password, no host', () => { + const attributes = getConnectionAttributesFromUrl('amqp://user:pass@'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.MESSAGING_URL]: `amqp://user:***@`, + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.MESSAGING_URL]: 'amqp://user:***@', }); }); - it("host only", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://host`); + it('host only', () => { + const attributes = getConnectionAttributesFromUrl('amqp://host'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'host', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp://host`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://host', }); }); - it("port only", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://:10000`); + it('port only', () => { + const attributes = getConnectionAttributesFromUrl('amqp://:10000'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.MESSAGING_URL]: `amqp://:10000`, + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.MESSAGING_URL]: 'amqp://:10000', }); }); - it("vhost only", function () { - const attributes = getConnectionAttributesFromUrl(`amqp:///vhost`); + it('vhost only', () => { + const attributes = getConnectionAttributesFromUrl('amqp:///vhost'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "localhost", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'localhost', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp:///vhost`, + [SemanticAttributes.MESSAGING_URL]: 'amqp:///vhost', }); }); - it("host only, trailing slash", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://host/`); + it('host only, trailing slash', () => { + const attributes = getConnectionAttributesFromUrl('amqp://host/'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'host', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp://host/`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://host/', }); }); - it("vhost encoded", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://host/%2f`); + it('vhost encoded', () => { + const attributes = getConnectionAttributesFromUrl('amqp://host/%2f'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "host", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: 'host', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp://host/%2f`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://host/%2f', }); }); - it("IPv6 host", function () { - const attributes = getConnectionAttributesFromUrl(`amqp://[::1]`); + it('IPv6 host', () => { + const attributes = getConnectionAttributesFromUrl('amqp://[::1]'); expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL]: "AMQP", - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: "0.9.1", - [SemanticAttributes.NET_PEER_NAME]: "[::1]", + [SemanticAttributes.MESSAGING_PROTOCOL]: 'AMQP', + [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', + [SemanticAttributes.NET_PEER_NAME]: '[::1]', [SemanticAttributes.NET_PEER_PORT]: 5672, - [SemanticAttributes.MESSAGING_URL]: `amqp://[::1]`, + [SemanticAttributes.MESSAGING_URL]: 'amqp://[::1]', }); }); }); diff --git a/plugins/node/instrumentation-amqplib/test/utils.ts b/plugins/node/instrumentation-amqplib/test/utils.ts index b5f9da8d86..88c268ca05 100644 --- a/plugins/node/instrumentation-amqplib/test/utils.ts +++ b/plugins/node/instrumentation-amqplib/test/utils.ts @@ -1,73 +1,110 @@ +/* + * 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. + */ import type * as amqp from 'amqplib'; import type * as amqpCallback from 'amqplib/callback_api'; import * as expect from 'expect'; -import { TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, TEST_RABBITMQ_USER } from './config'; +import { + TEST_RABBITMQ_HOST, + TEST_RABBITMQ_PASS, + TEST_RABBITMQ_PORT, + TEST_RABBITMQ_USER, +} from './config'; -export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; +export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; export const asyncConfirmSend = ( - confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, - queueName: string, - msgPayload: string, - callback?: () => void + confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, + queueName: string, + msgPayload: string, + callback?: () => void ): Promise => { - return new Promise((resolve, reject) => { - const hadSpaceInBuffer = confirmChannel.sendToQueue(queueName, Buffer.from(msgPayload), {}, (err) => { - try { - callback?.(); - resolve(); - } catch (e) { - reject(e); - } - }); - expect(hadSpaceInBuffer).toBeTruthy(); - }); + return new Promise((resolve, reject) => { + const hadSpaceInBuffer = confirmChannel.sendToQueue( + queueName, + Buffer.from(msgPayload), + {}, + err => { + try { + callback?.(); + resolve(); + } catch (e) { + reject(e); + } + } + ); + expect(hadSpaceInBuffer).toBeTruthy(); + }); }; export const asyncConfirmPublish = ( - confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, - exchange: string, - routingKey: string, - msgPayload: string, - callback?: () => void + confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, + exchange: string, + routingKey: string, + msgPayload: string, + callback?: () => void ): Promise => { - return new Promise((resolve, reject) => { - const hadSpaceInBuffer = confirmChannel.publish(exchange, routingKey, Buffer.from(msgPayload), {}, (err) => { - try { - callback?.(); - resolve(); - } catch (e) { - reject(e); - } - }); - expect(hadSpaceInBuffer).toBeTruthy(); - }); + return new Promise((resolve, reject) => { + const hadSpaceInBuffer = confirmChannel.publish( + exchange, + routingKey, + Buffer.from(msgPayload), + {}, + err => { + try { + callback?.(); + resolve(); + } catch (e) { + reject(e); + } + } + ); + expect(hadSpaceInBuffer).toBeTruthy(); + }); }; export const asyncConsume = ( - channel: amqp.Channel | amqpCallback.Channel | amqp.ConfirmChannel | amqpCallback.ConfirmChannel, - queueName: string, - callback: ( ((msg: amqp.Message) => unknown) | null)[], - options?: amqp.Options.Consume + channel: + | amqp.Channel + | amqpCallback.Channel + | amqp.ConfirmChannel + | amqpCallback.ConfirmChannel, + queueName: string, + callback: (((msg: amqp.Message) => unknown) | null)[], + options?: amqp.Options.Consume ): Promise => { - const msgs: amqp.Message[] = []; - return new Promise((resolve) => - channel.consume( - queueName, - (msg) => { - if(!msg) { throw Error('received null msg')} - msgs.push(msg); - try { - callback[msgs.length - 1]?.(msg); - if (msgs.length >= callback.length) { - setImmediate(() => resolve(msgs)); - } - } catch (err) { - setImmediate(() => resolve(msgs)); - throw err; - } - }, - options - ) - ); + const msgs: amqp.Message[] = []; + return new Promise(resolve => + channel.consume( + queueName, + msg => { + if (!msg) { + throw Error('received null msg'); + } + msgs.push(msg); + try { + callback[msgs.length - 1]?.(msg); + if (msgs.length >= callback.length) { + setImmediate(() => resolve(msgs)); + } + } catch (err) { + setImmediate(() => resolve(msgs)); + throw err; + } + }, + options + ) + ); }; From e633feea95cded296ee60ff8b6c95e65cb7f0c7a Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:32:29 +0200 Subject: [PATCH 03/39] chore(amqplib): add to release please --- .release-please-manifest.json | 47 ++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 47b1adabb2..431fd06d84 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1,46 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.26.2","detectors/node/opentelemetry-resource-detector-aws":"1.0.3","detectors/node/opentelemetry-resource-detector-gcp":"0.26.2","detectors/node/opentelemetry-resource-detector-github":"0.26.1","metapackages/auto-instrumentations-node":"0.27.3","metapackages/auto-instrumentations-web":"0.27.2","packages/opentelemetry-browser-extension-autoinjection":"0.27.3","packages/opentelemetry-host-metrics":"0.27.1","packages/opentelemetry-id-generator-aws-xray":"1.0.1","packages/opentelemetry-test-utils":"0.29.0","plugins/node/instrumentation-tedious":"0.1.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.29.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.5.0","plugins/node/opentelemetry-instrumentation-bunyan":"0.27.1","plugins/node/opentelemetry-instrumentation-cassandra":"0.27.1","plugins/node/opentelemetry-instrumentation-connect":"0.27.1","plugins/node/opentelemetry-instrumentation-dns":"0.27.1","plugins/node/opentelemetry-instrumentation-express":"0.28.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.27.2","plugins/node/opentelemetry-instrumentation-graphql":"0.27.3","plugins/node/opentelemetry-instrumentation-hapi":"0.27.1","plugins/node/opentelemetry-instrumentation-ioredis":"0.27.1","plugins/node/opentelemetry-instrumentation-knex":"0.27.1","plugins/node/opentelemetry-instrumentation-koa":"0.28.1","plugins/node/opentelemetry-instrumentation-memcached":"0.27.1","plugins/node/opentelemetry-instrumentation-mongodb":"0.28.0","plugins/node/opentelemetry-instrumentation-mysql":"0.27.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.28.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.28.3","plugins/node/opentelemetry-instrumentation-net":"0.27.1","plugins/node/opentelemetry-instrumentation-pg":"0.28.0","plugins/node/opentelemetry-instrumentation-pino":"0.28.1","plugins/node/opentelemetry-instrumentation-redis":"0.28.0","plugins/node/opentelemetry-instrumentation-restify":"0.27.2","plugins/node/opentelemetry-instrumentation-router":"0.27.1","plugins/node/opentelemetry-instrumentation-winston":"0.27.1","plugins/web/opentelemetry-instrumentation-document-load":"0.27.1","plugins/web/opentelemetry-instrumentation-user-interaction":"0.28.1","plugins/web/opentelemetry-plugin-react-load":"0.26.1","propagators/opentelemetry-propagator-aws-xray":"1.0.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.25.1","propagators/opentelemetry-propagator-ot-trace":"0.25.1","plugins/node/opentelemetry-instrumentation-fastify":"0.25.0"} \ No newline at end of file +{ + "detectors/node/opentelemetry-resource-detector-alibaba-cloud": "0.26.2", + "detectors/node/opentelemetry-resource-detector-aws": "1.0.3", + "detectors/node/opentelemetry-resource-detector-gcp": "0.26.2", + "detectors/node/opentelemetry-resource-detector-github": "0.26.1", + "metapackages/auto-instrumentations-node": "0.27.3", + "metapackages/auto-instrumentations-web": "0.27.2", + "packages/opentelemetry-browser-extension-autoinjection": "0.27.3", + "packages/opentelemetry-host-metrics": "0.27.1", + "packages/opentelemetry-id-generator-aws-xray": "1.0.1", + "packages/opentelemetry-test-utils": "0.29.0", + "plugins/node/instrumentation-amwplib": "0.27.0", + "plugins/node/instrumentation-tedious": "0.1.0", + "plugins/node/opentelemetry-instrumentation-aws-lambda": "0.29.0", + "plugins/node/opentelemetry-instrumentation-aws-sdk": "0.5.0", + "plugins/node/opentelemetry-instrumentation-bunyan": "0.27.1", + "plugins/node/opentelemetry-instrumentation-cassandra": "0.27.1", + "plugins/node/opentelemetry-instrumentation-connect": "0.27.1", + "plugins/node/opentelemetry-instrumentation-dns": "0.27.1", + "plugins/node/opentelemetry-instrumentation-express": "0.28.0", + "plugins/node/opentelemetry-instrumentation-generic-pool": "0.27.2", + "plugins/node/opentelemetry-instrumentation-graphql": "0.27.3", + "plugins/node/opentelemetry-instrumentation-hapi": "0.27.1", + "plugins/node/opentelemetry-instrumentation-ioredis": "0.27.1", + "plugins/node/opentelemetry-instrumentation-knex": "0.27.1", + "plugins/node/opentelemetry-instrumentation-koa": "0.28.1", + "plugins/node/opentelemetry-instrumentation-memcached": "0.27.1", + "plugins/node/opentelemetry-instrumentation-mongodb": "0.28.0", + "plugins/node/opentelemetry-instrumentation-mysql": "0.27.1", + "plugins/node/opentelemetry-instrumentation-mysql2": "0.28.0", + "plugins/node/opentelemetry-instrumentation-nestjs-core": "0.28.3", + "plugins/node/opentelemetry-instrumentation-net": "0.27.1", + "plugins/node/opentelemetry-instrumentation-pg": "0.28.0", + "plugins/node/opentelemetry-instrumentation-pino": "0.28.1", + "plugins/node/opentelemetry-instrumentation-redis": "0.28.0", + "plugins/node/opentelemetry-instrumentation-restify": "0.27.2", + "plugins/node/opentelemetry-instrumentation-router": "0.27.1", + "plugins/node/opentelemetry-instrumentation-winston": "0.27.1", + "plugins/web/opentelemetry-instrumentation-document-load": "0.27.1", + "plugins/web/opentelemetry-instrumentation-user-interaction": "0.28.1", + "plugins/web/opentelemetry-plugin-react-load": "0.26.1", + "propagators/opentelemetry-propagator-aws-xray": "1.0.1", + "propagators/opentelemetry-propagator-grpc-census-binary": "0.25.1", + "propagators/opentelemetry-propagator-ot-trace": "0.25.1", + "plugins/node/opentelemetry-instrumentation-fastify": "0.25.0" +} \ No newline at end of file From bea85831b5edefb4db6340dfa0fe307edfd8caa0 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:32:51 +0200 Subject: [PATCH 04/39] docs(amqplib): add instrumentation to main README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4402881b3c..c4adb2e3f6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ OpenTelemetry can collect tracing data automatically using instrumentations. Ven ### Node Instrumentations +- [@opentelemetry/instrumentation-amqplib][otel-contrib-instrumentation-amqplib] - [@opentelemetry/instrumentation-aws-lambda][otel-contrib-instrumentation-aws-lambda] - [@opentelemetry/instrumentation-aws-sdk][otel-contrib-instrumentation-aws-sdk] - [@opentelemetry/instrumentation-bunyan][otel-contrib-instrumentation-bunyan] @@ -160,6 +161,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [otel-instrumentation-http]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http [otel-instrumentation-xml-http-request]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-xml-http-request +[otel-contrib-instrumentation-amqplib]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-amqplib [otel-contrib-instrumentation-aws-lambda]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-aws-lambda [otel-contrib-instrumentation-aws-sdk]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-aws-sdk [otel-contrib-instrumentation-bunyan]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-bunyan From 8d2a9c108e19100f5d6b36bcfa090636e185c090 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:33:10 +0200 Subject: [PATCH 05/39] chore(amqplib): set component owner --- .github/component_owners.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 155822db67..e86da84d53 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -14,6 +14,8 @@ components: packages/opentelemetry-id-generator-aws-xray: - NathanielRN - willarmiros + plugins/node/instrumentation-amqplib: + - blumamir plugins/node/instrumentation-tedious: - rauno56 plugins/node/opentelemetry-instrumentation-aws-lambda: From f6ca8e6addfcccc4b36c68d15e40aaeb7e143c4b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:33:40 +0200 Subject: [PATCH 06/39] feat(auto-instrumentation-node): ass amqplib --- metapackages/auto-instrumentations-node/package.json | 1 + metapackages/auto-instrumentations-node/src/utils.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index 69ceba5474..2b94da7c95 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@opentelemetry/instrumentation": "^0.27.0", + "@opentelemetry/instrumentation-amqplib": "^0.27.0", "@opentelemetry/instrumentation-aws-lambda": "^0.29.0", "@opentelemetry/instrumentation-aws-sdk": "^0.5.0", "@opentelemetry/instrumentation-bunyan": "^0.27.0", diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index 1f705172b6..38dd2da472 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -16,6 +16,7 @@ import { diag } from '@opentelemetry/api'; import { Instrumentation } from '@opentelemetry/instrumentation'; +import { AmqplibInstrumentation } from '@opentelemetry/instrumentation-amqplib'; import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda'; import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan'; @@ -45,6 +46,7 @@ import { RestifyInstrumentation } from '@opentelemetry/instrumentation-restify'; import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston'; const InstrumentationMap = { + '@opentelemetry/instrumentation-amqplib': AmqplibInstrumentation, '@opentelemetry/instrumentation-aws-lambda': AwsLambdaInstrumentation, '@opentelemetry/instrumentation-aws-sdk': AwsInstrumentation, '@opentelemetry/instrumentation-bunyan': BunyanInstrumentation, From 89c4f50cb030c99fe3cb3fda34729cdd119de954 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:48:46 +0200 Subject: [PATCH 07/39] fix(amqplib): no support for node8 --- .github/workflows/unit-test.yml | 1 + plugins/node/instrumentation-amqplib/package.json | 2 +- plugins/node/instrumentation-amqplib/src/utils.ts | 8 ++++---- plugins/node/instrumentation-amqplib/test/utils.test.ts | 8 -------- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index e05cd45729..3ced11a9fc 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -16,6 +16,7 @@ jobs: --ignore @opentelemetry/instrumentation-aws-sdk --ignore @opentelemetry/instrumentation-pino --ignore @opentelemetry/instrumentation-tedious + --ignore @opentelemetry/instrumentation-amqplib - node: "10" lerna-extra-args: >- --ignore @opentelemetry/instrumentation-pino diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json index 9a6568b75f..65067a54e1 100644 --- a/plugins/node/instrumentation-amqplib/package.json +++ b/plugins/node/instrumentation-amqplib/package.json @@ -73,6 +73,6 @@ "typescript": "4.3.5" }, "engines": { - "node": ">=8.5.0" + "node": ">=10.0.0" } } \ No newline at end of file diff --git a/plugins/node/instrumentation-amqplib/src/utils.ts b/plugins/node/instrumentation-amqplib/src/utils.ts index 85e9cbcba0..2e6e77c766 100644 --- a/plugins/node/instrumentation-amqplib/src/utils.ts +++ b/plugins/node/instrumentation-amqplib/src/utils.ts @@ -23,7 +23,6 @@ import { } from '@opentelemetry/api'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type * as amqp from 'amqplib'; -import * as urlLib from 'url'; export const MESSAGE_STORED_SPAN: unique symbol = Symbol( 'opentelemetry.amqplib.message.stored-span' @@ -74,7 +73,7 @@ const getPort = ( return portFromUrl || (resolvedProtocol === 'AMQP' ? 5672 : 5671); }; -const getProtocol = (protocolFromUrl: string | null): string => { +const getProtocol = (protocolFromUrl: string | undefined): string => { const resolvedProtocol = protocolFromUrl || 'amqp'; // the substring removed the ':' part of the protocol ('amqp:' -> 'amqp') const noEndingColon = resolvedProtocol.endsWith(':') @@ -84,7 +83,7 @@ const getProtocol = (protocolFromUrl: string | null): string => { return noEndingColon.toUpperCase(); }; -const getHostname = (hostnameFromUrl: string | null): string => { +const getHostname = (hostnameFromUrl: string | undefined): string => { // if user supplies empty hostname, it gets forwarded to 'net' package which default it to localhost. // https://nodejs.org/docs/latest-v12.x/api/net.html#net_socket_connect_options_connectlistener return hostnameFromUrl || 'localhost'; @@ -166,7 +165,7 @@ export const getConnectionAttributesFromUrl = ( const censoredUrl = censorPassword(url); attributes[SemanticAttributes.MESSAGING_URL] = censoredUrl; try { - const urlParts = urlLib.parse(censoredUrl); + const urlParts = new URL(censoredUrl); const protocol = getProtocol(urlParts.protocol); Object.assign(attributes, { @@ -201,6 +200,7 @@ export const getConnectionAttributesFromUrl = ( ), }); } catch (err) { + console.log(err); diag.error( 'amqplib instrumentation: error while extracting connection details from connection url', { diff --git a/plugins/node/instrumentation-amqplib/test/utils.test.ts b/plugins/node/instrumentation-amqplib/test/utils.test.ts index d8286d640f..a2cb71a6a5 100644 --- a/plugins/node/instrumentation-amqplib/test/utils.test.ts +++ b/plugins/node/instrumentation-amqplib/test/utils.test.ts @@ -115,14 +115,6 @@ describe('utils', () => { }); }); - it('port only', () => { - const attributes = getConnectionAttributesFromUrl('amqp://:10000'); - expect(attributes).toStrictEqual({ - [SemanticAttributes.MESSAGING_PROTOCOL_VERSION]: '0.9.1', - [SemanticAttributes.MESSAGING_URL]: 'amqp://:10000', - }); - }); - it('vhost only', () => { const attributes = getConnectionAttributesFromUrl('amqp:///vhost'); expect(attributes).toStrictEqual({ From 6fd6febc97992fca08d9573f287d5e4e8b146349 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 14:49:57 +0200 Subject: [PATCH 08/39] fix(amqplib): remove console log --- plugins/node/instrumentation-amqplib/src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/src/utils.ts b/plugins/node/instrumentation-amqplib/src/utils.ts index 2e6e77c766..4f7f479c96 100644 --- a/plugins/node/instrumentation-amqplib/src/utils.ts +++ b/plugins/node/instrumentation-amqplib/src/utils.ts @@ -200,7 +200,6 @@ export const getConnectionAttributesFromUrl = ( ), }); } catch (err) { - console.log(err); diag.error( 'amqplib instrumentation: error while extracting connection details from connection url', { From 12a95a733868b950e766fd55eedf3aada810c7d8 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:04:02 +0200 Subject: [PATCH 09/39] feat: change hook signature with info pattern --- .../instrumentation-amqplib/src/amqplib.ts | 6 +-- .../node/instrumentation-amqplib/src/types.ts | 26 ++++++++--- .../test/amqplib-promise.test.ts | 43 ++++++++----------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 8992269545..826648a393 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -439,7 +439,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (self._config.consumeHook) { safeExecuteInTheMiddle( - () => self._config.consumeHook!(span, msg), + () => self._config.consumeHook!(span, { msg }), e => { if (e) { diag.error('amqplib instrumentation: consumerHook error', e); @@ -539,8 +539,8 @@ export class AmqplibInstrumentation extends InstrumentationBase { content, options, isConfirmChannel: true, + confirmError: err, }, - err ), e => { if (e) { @@ -729,7 +729,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (!this._config.consumeEndHook) return; safeExecuteInTheMiddle( - () => this._config.consumeEndHook!(span, msg, rejected, endOperation), + () => this._config.consumeEndHook!(span, {msg, rejected, endOperation}), e => { if (e) { diag.error('amqplib instrumentation: consumerEndHook error', e); diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts index bc2022d8f4..ebdbae6ff0 100644 --- a/plugins/node/instrumentation-amqplib/src/types.ts +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -17,7 +17,7 @@ import { Span } from '@opentelemetry/api'; import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type * as amqp from 'amqplib'; -export interface PublishParams { +export interface PublishInfo { exchange: string; routingKey: string; content: Buffer; @@ -25,24 +25,36 @@ export interface PublishParams { isConfirmChannel?: boolean; } +export interface PublishConfirmedInfo extends PublishInfo { + confirmError?: any; +} + +export interface ConsumeInfo { + msg: amqp.ConsumeMessage; +} + +export interface ConsumeEndInfo { + msg: amqp.ConsumeMessage; + rejected: boolean | null; + endOperation: EndOperation; +} + export interface AmqplibPublishCustomAttributeFunction { - (span: Span, publishParams: PublishParams): void; + (span: Span, publishInfo: PublishInfo): void; } export interface AmqplibConfirmCustomAttributeFunction { - (span: Span, publishParams: PublishParams, confirmError: any): void; + (span: Span, publishConfirmedInto: PublishConfirmedInfo): void; } export interface AmqplibConsumerCustomAttributeFunction { - (span: Span, msg: amqp.ConsumeMessage): void; + (span: Span, consumeInfo: ConsumeInfo): void; } export interface AmqplibConsumerEndCustomAttributeFunction { ( span: Span, - msg: amqp.ConsumeMessage, - rejected: boolean | null, - endOperation: EndOperation + consumeEndInfo: ConsumeEndInfo, ): void; } diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index 2db1bf653f..179dd7a9eb 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -17,7 +17,7 @@ import 'mocha'; import * as expect from 'expect'; import * as sinon from 'sinon'; import * as lodash from 'lodash'; -import { AmqplibInstrumentation, EndOperation, PublishParams } from '../src'; +import { AmqplibInstrumentation, ConsumeEndInfo, ConsumeInfo, EndOperation, PublishInfo } from '../src'; import { getTestSpans, registerInstrumentationTesting, @@ -70,12 +70,12 @@ describe('amqplib instrumentation promise model', () => { expect(endHookSpy.callCount).toBe(expectedEndOperations.length); expectedEndOperations.forEach( (endOperation: EndOperation, index: number) => { - expect(endHookSpy.args[index][3]).toEqual(endOperation); + expect(endHookSpy.args[index][1].endOperation).toEqual(endOperation); switch (endOperation) { case EndOperation.AutoAck: case EndOperation.Ack: case EndOperation.AckAll: - expect(endHookSpy.args[index][2]).toBeFalsy(); + expect(endHookSpy.args[index][1].rejected).toBeFalsy(); break; case EndOperation.Reject: @@ -83,7 +83,7 @@ describe('amqplib instrumentation promise model', () => { case EndOperation.NackAll: case EndOperation.ChannelClosed: case EndOperation.ChannelError: - expect(endHookSpy.args[index][2]).toBeTruthy(); + expect(endHookSpy.args[index][1].rejected).toBeTruthy(); break; } } @@ -579,24 +579,21 @@ describe('amqplib instrumentation promise model', () => { const attributeNameFromEndHook = 'attribute.name.from.endhook'; const endHookAttributeValue = 'attribute value from end hook'; instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { + publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); expect(publishParams.exchange).toEqual(''); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(msg!.content.toString()).toEqual(msgPayload); + expect(consumeInfo.msg!.content.toString()).toEqual(msgPayload); }, consumeEndHook: ( span: Span, - msg: amqp.ConsumeMessage | null, - rejected: boolean | null, - endOperation: EndOperation - ): void => { + consumeEndInfo: ConsumeEndInfo): void => { span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); - expect(endOperation).toEqual(EndOperation.AutoAck); + expect(consumeEndInfo.endOperation).toEqual(EndOperation.AutoAck); }, }); @@ -621,11 +618,11 @@ describe('amqplib instrumentation promise model', () => { const attributeNameFromHook = 'attribute.name.from.hook'; const hookAttributeValue = 'attribute value from hook'; instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { + publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); throw new Error('error from hook'); }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); throw new Error('error from hook'); }, @@ -1206,7 +1203,7 @@ describe('amqplib instrumentation promise model', () => { const consumeEndHookAttributeValue = 'attribute value from consume end hook'; instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams) => { + publishHook: (span: Span, publishParams: PublishInfo) => { span.setAttribute(attributeNameFromHook, hookAttributeValue); expect(publishParams.exchange).toEqual(''); expect(publishParams.routingKey).toEqual(queueName); @@ -1222,21 +1219,19 @@ describe('amqplib instrumentation promise model', () => { expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null) => { + consumeHook: (span: Span, consumeInfo: ConsumeInfo) => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(msg!.content.toString()).toEqual(msgPayload); + expect(consumeInfo.msg!.content.toString()).toEqual(msgPayload); }, consumeEndHook: ( span: Span, - msg: amqp.ConsumeMessage | null, - rejected: boolean | null, - endOperation: EndOperation + consumeEndInfo: ConsumeEndInfo ): void => { span.setAttribute( attributeNameFromConsumeEndHook, consumeEndHookAttributeValue ); - expect(endOperation).toEqual(EndOperation.AutoAck); + expect(consumeEndInfo.endOperation).toEqual(EndOperation.AutoAck); }, }); @@ -1264,18 +1259,18 @@ describe('amqplib instrumentation promise model', () => { const attributeNameFromHook = 'attribute.name.from.hook'; const hookAttributeValue = 'attribute value from hook'; instrumentation.setConfig({ - publishHook: (span: Span, publishParams: PublishParams): void => { + publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); throw new Error('error from hook'); }, publishConfirmHook: ( span: Span, - publishParams: PublishParams + publishParams: PublishInfo ): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); throw new Error('error from hook'); }, - consumeHook: (span: Span, msg: amqp.ConsumeMessage | null): void => { + consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); throw new Error('error from hook'); }, From 86135e15258c2cf0d7d8df614a7a2587e4206b55 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:13:17 +0200 Subject: [PATCH 10/39] refactor(amqplib): move instrumented module version to hook --- .../instrumentation-amqplib/src/amqplib.ts | 38 ++++-------- .../node/instrumentation-amqplib/src/types.ts | 23 +++----- .../test/amqplib-promise.test.ts | 59 +++---------------- 3 files changed, 29 insertions(+), 91 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 826648a393..c598529ffc 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -430,16 +430,9 @@ export class AmqplibInstrumentation extends InstrumentationBase { parentContext ); - if (self._config.moduleVersionAttributeName && moduleVersion) { - span.setAttribute( - self._config.moduleVersionAttributeName, - moduleVersion - ); - } - if (self._config.consumeHook) { safeExecuteInTheMiddle( - () => self._config.consumeHook!(span, { msg }), + () => self._config.consumeHook!(span, { moduleVersion, msg }), e => { if (e) { diag.error('amqplib instrumentation: consumerHook error', e); @@ -497,7 +490,6 @@ export class AmqplibInstrumentation extends InstrumentationBase { exchange, routingKey, channel, - moduleVersion, options ); @@ -505,6 +497,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { safeExecuteInTheMiddle( () => self._config.publishHook!(span, { + moduleVersion, exchange, routingKey, content, @@ -531,17 +524,14 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (self._config.publishConfirmHook) { safeExecuteInTheMiddle( () => - self._config.publishConfirmHook!( - span, - { - exchange, - routingKey, - content, - options, - isConfirmChannel: true, - confirmError: err, - }, - ), + self._config.publishConfirmHook!(span, { + exchange, + routingKey, + content, + options, + isConfirmChannel: true, + confirmError: err, + }), e => { if (e) { diag.error( @@ -599,7 +589,6 @@ export class AmqplibInstrumentation extends InstrumentationBase { exchange, routingKey, channel, - moduleVersion, options ); @@ -607,6 +596,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { safeExecuteInTheMiddle( () => self._config.publishHook!(span, { + moduleVersion, exchange, routingKey, content, @@ -638,7 +628,6 @@ export class AmqplibInstrumentation extends InstrumentationBase { exchange: string, routingKey: string, channel: InstrumentationPublishChannel, - moduleVersion: string | undefined, options?: amqp.Options.Publish ) { const normalizedExchange = normalizeExchange(exchange); @@ -659,9 +648,6 @@ export class AmqplibInstrumentation extends InstrumentationBase { }, } ); - if (self._config.moduleVersionAttributeName && moduleVersion) { - span.setAttribute(self._config.moduleVersionAttributeName, moduleVersion); - } const modifiedOptions = options ?? {}; modifiedOptions.headers = modifiedOptions.headers ?? {}; @@ -729,7 +715,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (!this._config.consumeEndHook) return; safeExecuteInTheMiddle( - () => this._config.consumeEndHook!(span, {msg, rejected, endOperation}), + () => this._config.consumeEndHook!(span, { msg, rejected, endOperation }), e => { if (e) { diag.error('amqplib instrumentation: consumerEndHook error', e); diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts index ebdbae6ff0..968bdd1b3f 100644 --- a/plugins/node/instrumentation-amqplib/src/types.ts +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -18,6 +18,7 @@ import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type * as amqp from 'amqplib'; export interface PublishInfo { + moduleVersion: string | undefined; exchange: string; routingKey: string; content: Buffer; @@ -26,17 +27,18 @@ export interface PublishInfo { } export interface PublishConfirmedInfo extends PublishInfo { - confirmError?: any; + confirmError?: any; } export interface ConsumeInfo { - msg: amqp.ConsumeMessage; + moduleVersion: string | undefined; + msg: amqp.ConsumeMessage; } export interface ConsumeEndInfo { - msg: amqp.ConsumeMessage; - rejected: boolean | null; - endOperation: EndOperation; + msg: amqp.ConsumeMessage; + rejected: boolean | null; + endOperation: EndOperation; } export interface AmqplibPublishCustomAttributeFunction { @@ -52,10 +54,7 @@ export interface AmqplibConsumerCustomAttributeFunction { } export interface AmqplibConsumerEndCustomAttributeFunction { - ( - span: Span, - consumeEndInfo: ConsumeEndInfo, - ): void; + (span: Span, consumeEndInfo: ConsumeEndInfo): void; } export enum EndOperation { @@ -83,12 +82,6 @@ export interface AmqplibInstrumentationConfig extends InstrumentationConfig { /** hook for adding custom attributes after consumer message is acked to server */ consumeEndHook?: AmqplibConsumerEndCustomAttributeFunction; - /** - * If passed, a span attribute will be added to all spans with key of the provided "moduleVersionAttributeName" - * and value of the module version. - */ - moduleVersionAttributeName?: string; - /** * When user is setting up consume callback, it is user's responsibility to call * ack/nack etc on the msg to resolve it in the server. diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index 179dd7a9eb..335bd805fa 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -17,7 +17,13 @@ import 'mocha'; import * as expect from 'expect'; import * as sinon from 'sinon'; import * as lodash from 'lodash'; -import { AmqplibInstrumentation, ConsumeEndInfo, ConsumeInfo, EndOperation, PublishInfo } from '../src'; +import { + AmqplibInstrumentation, + ConsumeEndInfo, + ConsumeInfo, + EndOperation, + PublishInfo, +} from '../src'; import { getTestSpans, registerInstrumentationTesting, @@ -548,30 +554,6 @@ describe('amqplib instrumentation promise model', () => { }); }); - it('moduleVersionAttributeName works with publish and consume', async () => { - const VERSION_ATTR = 'module.version'; - instrumentation.setConfig({ - moduleVersionAttributeName: VERSION_ATTR, - }); - - channel.sendToQueue(queueName, Buffer.from(msgPayload)); - - await asyncConsume( - channel, - queueName, - [msg => expect(msg.content.toString()).toEqual(msgPayload)], - { - noAck: true, - } - ); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach(s => - expect(s.attributes[VERSION_ATTR]).toMatch( - /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ - ) - ); - }); - describe('hooks', () => { it('publish and consume hooks success', async () => { const attributeNameFromHook = 'attribute.name.from.hook'; @@ -591,7 +573,8 @@ describe('amqplib instrumentation promise model', () => { }, consumeEndHook: ( span: Span, - consumeEndInfo: ConsumeEndInfo): void => { + consumeEndInfo: ConsumeEndInfo + ): void => { span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); expect(consumeEndInfo.endOperation).toEqual(EndOperation.AutoAck); }, @@ -1166,30 +1149,6 @@ describe('amqplib instrumentation promise model', () => { }); }); - it('moduleVersionAttributeName works with publish and consume', async () => { - const VERSION_ATTR = 'module.version'; - instrumentation.setConfig({ - moduleVersionAttributeName: VERSION_ATTR, - }); - - await asyncConfirmSend(confirmChannel, queueName, msgPayload); - - await asyncConsume( - confirmChannel, - queueName, - [msg => expect(msg.content.toString()).toEqual(msgPayload)], - { - noAck: true, - } - ); - expect(getTestSpans().length).toBe(2); - getTestSpans().forEach(s => - expect(s.attributes[VERSION_ATTR]).toMatch( - /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ - ) - ); - }); - describe('hooks', () => { it('publish and consume hooks success', async () => { const attributeNameFromHook = 'attribute.name.from.hook'; From 6dfcb1244ae20feffd0eede268ad2880ab75e979 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:16:21 +0200 Subject: [PATCH 11/39] fix(amqplib): add missing moduleVersion in publish hook --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index c598529ffc..a71b3be2c4 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -525,6 +525,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { safeExecuteInTheMiddle( () => self._config.publishConfirmHook!(span, { + moduleVersion, exchange, routingKey, content, From b67f20f0224746c3380e28cbd56b37239038c953 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:37:02 +0200 Subject: [PATCH 12/39] docs(amqplib): add README for instrumentation --- plugins/node/instrumentation-amqplib/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts index 968bdd1b3f..57626b524f 100644 --- a/plugins/node/instrumentation-amqplib/src/types.ts +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -45,7 +45,7 @@ export interface AmqplibPublishCustomAttributeFunction { (span: Span, publishInfo: PublishInfo): void; } -export interface AmqplibConfirmCustomAttributeFunction { +export interface AmqplibPublishConfirmCustomAttributeFunction { (span: Span, publishConfirmedInto: PublishConfirmedInfo): void; } @@ -74,7 +74,7 @@ export interface AmqplibInstrumentationConfig extends InstrumentationConfig { publishHook?: AmqplibPublishCustomAttributeFunction; /** hook for adding custom attributes after publish message is confirmed by the broker */ - publishConfirmHook?: AmqplibConfirmCustomAttributeFunction; + publishConfirmHook?: AmqplibPublishConfirmCustomAttributeFunction; /** hook for adding custom attributes before consumer message is processed */ consumeHook?: AmqplibConsumerCustomAttributeFunction; @@ -87,7 +87,7 @@ export interface AmqplibInstrumentationConfig extends InstrumentationConfig { * ack/nack etc on the msg to resolve it in the server. * If user is not calling the ack, the message will stay in the queue until * channel is closed, or until server timeout expires (if configured). - * While we wait for the ack, a copy of the message is stored in plugin, which + * While we wait for the ack, a reference to the message is stored in plugin, which * will never be garbage collected. * To prevent memory leak, plugin has it's own configuration of timeout, which * will close the span if user did not call ack after this timeout. From 3c891d783cd19979ae56d0051670b6a636f22e70 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:41:03 +0200 Subject: [PATCH 13/39] docs(amqplib): add missing README file to git --- .../node/instrumentation-amqplib/README.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 plugins/node/instrumentation-amqplib/README.md diff --git a/plugins/node/instrumentation-amqplib/README.md b/plugins/node/instrumentation-amqplib/README.md new file mode 100644 index 0000000000..f62df83b99 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/README.md @@ -0,0 +1,98 @@ +# OpenTelemetry Amqplib (RabbitMQ) Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for [`amqplib`](https://www.npmjs.com/package/amqplib) (RabbitMQ) + +For automatic instrumentation see the +[`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-node) package. + +Compatible with OpenTelemetry JS API and SDK `1.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-amqplib +``` + +## Supported Versions + +- `>=0.5.5` + +## Usage + +OpenTelemetry amqplib Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems when working with [`amqplib`](https://github.com/amqp-node/amqplib) (RabbitMQ). + +To load a specific plugin, specify it in the registerInstrumentations's configuration: + +```js +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { AmqplibInstrumentation } = require('@opentelemetry/instrumentation-amqplib'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new AmqplibInstrumentation({ + // publishHook: (span: Span, publishInfo: PublishInfo) => { }, + // publishConfirmHook: (span: Span, publishConfirmedInto: PublishConfirmedInfo) => { }, + // consumeHook: (span: Span, consumeInfo: ConsumeInfo) => { }, + // consumeEndHook: (span: Span, consumeEndInfo: ConsumeEndInfo) => { }, + }), + ], +}) +``` + +### amqplib Instrumentation Options + +amqplib instrumentation has few options available to choose from. You can set the following: + +| Options | Type | Description | +| --------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `publishHook` | `AmqplibPublishCustomAttributeFunction` | hook for adding custom attributes before publish message is sent. | +| `publishConfirmHook` | `AmqplibPublishConfirmCustomAttributeFunction` | hook for adding custom attributes after publish message is confirmed by the broker. | +| `consumeHook` | `AmqplibConsumerCustomAttributeFunction` | hook for adding custom attributes before consumer message is processed. | +| `consumeEndHook` | `AmqplibConsumerEndCustomAttributeFunction` | hook for adding custom attributes after consumer message is acked to server. | +| `consumeTimeoutMs` | `number` | read [Consume Timeout](#ConsumeTimeout) below | + +### Consume Timeout +When user is setting up consume callback, it is user's responsibility to call ack/nack etc on the msg to resolve it in the server. If user is not calling the ack, the message will stay in the queue until channel is closed, or until server timeout expires (if configured). + +While we wait for the ack, a reference to the message is stored in plugin, which +will never be garbage collected. +To prevent memory leak, plugin has it's own configuration of timeout, which will close the span if user did not call ack after this timeout. + +If timeout is not big enough, span might be closed with 'InstrumentationTimeout', and then received valid ack from the user later which will not be instrumented. + +Default is 1 minute + +## Migration From opentelemetry-instrumentation-amqplib + +This instrumentation was originally published under the name `"opentelemetry-instrumentation-amqplib"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). Few breaking changes were made during porting to the contrib repo to align with conventions: + +### Hook Info + +The instrumentation's config `publishHook`, `publishConfirmHook`, `consumeHook` and `consumeEndHook` functions signature changed, so the second function parameter is info object, containing the relevant hook data. + +### `moduleVersionAttributeName` config option + +The `moduleVersionAttributeName` config option is removed. To add the amqplib package version to spans, use the `moduleVersion` attribute in hook info for `publishHook` and `consumeHook` functions. + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-amqplib +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-amqplib.svg From 61f53a6e709d2a5d875c3e47b9e1f49149cd7e1b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:41:29 +0200 Subject: [PATCH 14/39] chore(amqplib): add test all versions configuration --- plugins/node/instrumentation-amqplib/.tav.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/node/instrumentation-amqplib/.tav.yml diff --git a/plugins/node/instrumentation-amqplib/.tav.yml b/plugins/node/instrumentation-amqplib/.tav.yml new file mode 100644 index 0000000000..bf4e3eed80 --- /dev/null +++ b/plugins/node/instrumentation-amqplib/.tav.yml @@ -0,0 +1,4 @@ +'amqplib': + versions: ">=0.5.5" + commands: + - yarn test From 9201ed990e08536acab02e2a218ea3da2b61b37b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:43:29 +0200 Subject: [PATCH 15/39] fix: revert husky file delete --- .husky/commit-msg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 .husky/commit-msg diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000000..e8511eaeaf --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit $1 From 6e4e11aba75a19370bcd6ae422ead0c0923db0a4 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:49:03 +0200 Subject: [PATCH 16/39] docs(amqplib): fix README lint --- plugins/node/instrumentation-amqplib/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/README.md b/plugins/node/instrumentation-amqplib/README.md index f62df83b99..ac201fd071 100644 --- a/plugins/node/instrumentation-amqplib/README.md +++ b/plugins/node/instrumentation-amqplib/README.md @@ -56,9 +56,10 @@ amqplib instrumentation has few options available to choose from. You can set th | `publishConfirmHook` | `AmqplibPublishConfirmCustomAttributeFunction` | hook for adding custom attributes after publish message is confirmed by the broker. | | `consumeHook` | `AmqplibConsumerCustomAttributeFunction` | hook for adding custom attributes before consumer message is processed. | | `consumeEndHook` | `AmqplibConsumerEndCustomAttributeFunction` | hook for adding custom attributes after consumer message is acked to server. | -| `consumeTimeoutMs` | `number` | read [Consume Timeout](#ConsumeTimeout) below | +| `consumeTimeoutMs` | `number` | read [Consume Timeout](#ConsumeTimeout) below | ### Consume Timeout + When user is setting up consume callback, it is user's responsibility to call ack/nack etc on the msg to resolve it in the server. If user is not calling the ack, the message will stay in the queue until channel is closed, or until server timeout expires (if configured). While we wait for the ack, a reference to the message is stored in plugin, which From 55aea9e0e1c8c7e70369b246431e5e199a5de4b5 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 16:59:20 +0200 Subject: [PATCH 17/39] fix(auto-instrumentations-node): fix test and add new inst to README --- metapackages/auto-instrumentations-node/README.md | 1 + metapackages/auto-instrumentations-node/test/utils.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/metapackages/auto-instrumentations-node/README.md b/metapackages/auto-instrumentations-node/README.md index bb33a67259..83f72a7e20 100644 --- a/metapackages/auto-instrumentations-node/README.md +++ b/metapackages/auto-instrumentations-node/README.md @@ -56,6 +56,7 @@ registerInstrumentations({ ## Supported instrumentations +- [@opentelemetry/instrumentation-amqplib](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-amqplib) - [@opentelemetry/instrumentation-aws-lambda](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-aws-lambda) - [@opentelemetry/instrumentation-aws-sdk](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-aws-sdk) - [@opentelemetry/instrumentation-bunyan](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-bunyan) diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index 9cbb59917f..e021c6f0c9 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -25,6 +25,7 @@ describe('utils', () => { it('should load default instrumentations', () => { const instrumentations = getNodeAutoInstrumentations(); const expectedInstrumentations = [ + '@opentelemetry/instrumentation-amqplib', '@opentelemetry/instrumentation-aws-lambda', '@opentelemetry/instrumentation-aws-sdk', '@opentelemetry/instrumentation-bunyan', From 2416e9c275c2a2ce3d4c45e67ab37d6c994dbaa8 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 17:02:55 +0200 Subject: [PATCH 18/39] chore(amqplib): add LICENSE --- plugins/node/instrumentation-amqplib/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json index 65067a54e1..c8d9320cec 100644 --- a/plugins/node/instrumentation-amqplib/package.json +++ b/plugins/node/instrumentation-amqplib/package.json @@ -20,7 +20,6 @@ "build/src/**/*.js", "build/src/**/*.js.map", "build/src/**/*.d.ts", - "doc", "LICENSE", "README.md" ], From 8a51b68345bc9b5d2ee09d14b6ce2fa8de40ae91 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 17:03:19 +0200 Subject: [PATCH 19/39] chore(amqplib): add LICENSE --- plugins/node/instrumentation-amqplib/LICENSE | 201 +++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 plugins/node/instrumentation-amqplib/LICENSE diff --git a/plugins/node/instrumentation-amqplib/LICENSE b/plugins/node/instrumentation-amqplib/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/instrumentation-amqplib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. From fb3afde657eee6f4251cf1c47da8ad97347f9992 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 17:29:01 +0200 Subject: [PATCH 20/39] refactor(amqplib): tests --- .../auto-instrumentations-node/test/utils.test.ts | 2 +- .../test/amqplib-callbacks.test.ts | 8 +++----- .../test/amqplib-connection.test.ts | 6 +++--- .../instrumentation-amqplib/test/amqplib-promise.test.ts | 8 +++----- plugins/node/instrumentation-amqplib/test/config.ts | 1 + plugins/node/instrumentation-amqplib/test/utils.test.ts | 2 +- plugins/node/instrumentation-amqplib/test/utils.ts | 8 -------- 7 files changed, 12 insertions(+), 23 deletions(-) diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index e021c6f0c9..e4b392da9e 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -54,7 +54,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-restify', '@opentelemetry/instrumentation-winston', ]; - assert.strictEqual(instrumentations.length, 27); + assert.strictEqual(instrumentations.length, 28); for (let i = 0, j = instrumentations.length; i < j; i++) { assert.strictEqual( instrumentations[i].instrumentationName, diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts index 317e95b6d9..e4ab04ad7e 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts @@ -31,21 +31,19 @@ import { import { context, SpanKind } from '@opentelemetry/api'; import { asyncConfirmSend, asyncConsume } from './utils'; import { + censoredUrl, + rabbitMqUrl, TEST_RABBITMQ_HOST, - TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, - TEST_RABBITMQ_USER, } from './config'; const msgPayload = 'payload from test'; const queueName = 'queue-name-from-unittest'; describe('amqplib instrumentation callback model', () => { - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; let conn: amqpCallback.Connection; before(done => { - amqpCallback.connect(url, (err, connection) => { + amqpCallback.connect(rabbitMqUrl, (err, connection) => { conn = connection; done(); }); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts index 27954039ab..e80738e310 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-connection.test.ts @@ -16,6 +16,8 @@ import 'mocha'; import * as expect from 'expect'; import { + censoredUrl, + rabbitMqUrl, TEST_RABBITMQ_HOST, TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, @@ -130,9 +132,7 @@ describe('amqplib instrumentation connection', () => { describe('connect with url string', () => { it('should extract connection attributes from url options', async function () { const testName = this.test!.title; - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const conn = await amqp.connect(url); + const conn = await amqp.connect(rabbitMqUrl); try { const channel = await conn.createChannel(); diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index 335bd805fa..9e7f93d330 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -42,10 +42,10 @@ import { import { Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from './utils'; import { + censoredUrl, + rabbitMqUrl, TEST_RABBITMQ_HOST, - TEST_RABBITMQ_PASS, TEST_RABBITMQ_PORT, - TEST_RABBITMQ_USER, } from './config'; import { SinonSpy } from 'sinon'; @@ -59,11 +59,9 @@ const CHANNEL_CLOSED_IN_TEST = Symbol( ); describe('amqplib instrumentation promise model', () => { - const url = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; - const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; let conn: amqp.Connection; before(async () => { - conn = await amqp.connect(url); + conn = await amqp.connect(rabbitMqUrl); }); after(async () => { await conn.close(); diff --git a/plugins/node/instrumentation-amqplib/test/config.ts b/plugins/node/instrumentation-amqplib/test/config.ts index 98c9cadd4c..3494d16e83 100644 --- a/plugins/node/instrumentation-amqplib/test/config.ts +++ b/plugins/node/instrumentation-amqplib/test/config.ts @@ -17,3 +17,4 @@ export const TEST_RABBITMQ_HOST = 'localhost'; export const TEST_RABBITMQ_PORT = 22221; export const TEST_RABBITMQ_USER = 'username'; export const TEST_RABBITMQ_PASS = 'password'; +export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; diff --git a/plugins/node/instrumentation-amqplib/test/utils.test.ts b/plugins/node/instrumentation-amqplib/test/utils.test.ts index a2cb71a6a5..57cd59bb23 100644 --- a/plugins/node/instrumentation-amqplib/test/utils.test.ts +++ b/plugins/node/instrumentation-amqplib/test/utils.test.ts @@ -21,7 +21,7 @@ import { } from '../src/utils'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import * as amqp from 'amqplib'; -import { rabbitMqUrl } from './utils'; +import { rabbitMqUrl } from './config'; describe('utils', () => { describe('getConnectionAttributesFromServer', () => { diff --git a/plugins/node/instrumentation-amqplib/test/utils.ts b/plugins/node/instrumentation-amqplib/test/utils.ts index 88c268ca05..42d68a7861 100644 --- a/plugins/node/instrumentation-amqplib/test/utils.ts +++ b/plugins/node/instrumentation-amqplib/test/utils.ts @@ -16,14 +16,6 @@ import type * as amqp from 'amqplib'; import type * as amqpCallback from 'amqplib/callback_api'; import * as expect from 'expect'; -import { - TEST_RABBITMQ_HOST, - TEST_RABBITMQ_PASS, - TEST_RABBITMQ_PORT, - TEST_RABBITMQ_USER, -} from './config'; - -export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; export const asyncConfirmSend = ( confirmChannel: amqp.ConfirmChannel | amqpCallback.ConfirmChannel, From abd1c3d93d45480dc32215e434633d6b49aa4389 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 17:33:24 +0200 Subject: [PATCH 21/39] fix(amqplib): missing file --- plugins/node/instrumentation-amqplib/test/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/node/instrumentation-amqplib/test/config.ts b/plugins/node/instrumentation-amqplib/test/config.ts index 3494d16e83..33ece23e97 100644 --- a/plugins/node/instrumentation-amqplib/test/config.ts +++ b/plugins/node/instrumentation-amqplib/test/config.ts @@ -18,3 +18,4 @@ export const TEST_RABBITMQ_PORT = 22221; export const TEST_RABBITMQ_USER = 'username'; export const TEST_RABBITMQ_PASS = 'password'; export const rabbitMqUrl = `amqp://${TEST_RABBITMQ_USER}:${TEST_RABBITMQ_PASS}@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; +export const censoredUrl = `amqp://${TEST_RABBITMQ_USER}:***@${TEST_RABBITMQ_HOST}:${TEST_RABBITMQ_PORT}`; From 6c2c8e6cb8a7870b375a0a48011561dc00047c37 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 17:49:15 +0200 Subject: [PATCH 22/39] fix(amqplib): hooks verify paraams before setAttribute --- .../test/amqplib-promise.test.ts | 402 +++++++++--------- 1 file changed, 201 insertions(+), 201 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index 9e7f93d330..8fb872219b 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -13,52 +13,52 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import 'mocha'; -import * as expect from 'expect'; -import * as sinon from 'sinon'; -import * as lodash from 'lodash'; +import "mocha"; +import * as expect from "expect"; +import * as sinon from "sinon"; +import * as lodash from "lodash"; import { AmqplibInstrumentation, ConsumeEndInfo, ConsumeInfo, EndOperation, PublishInfo, -} from '../src'; +} from "../src"; import { getTestSpans, registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; +} from "@opentelemetry/contrib-test-utils"; const instrumentation = registerInstrumentationTesting( new AmqplibInstrumentation() ); -import * as amqp from 'amqplib'; -import { ConsumeMessage } from 'amqplib'; +import * as amqp from "amqplib"; +import { ConsumeMessage } from "amqplib"; import { MessagingDestinationKindValues, SemanticAttributes, -} from '@opentelemetry/semantic-conventions'; -import { Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; -import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from './utils'; +} from "@opentelemetry/semantic-conventions"; +import { Span, SpanKind, SpanStatusCode } from "@opentelemetry/api"; +import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from "./utils"; import { censoredUrl, rabbitMqUrl, TEST_RABBITMQ_HOST, TEST_RABBITMQ_PORT, -} from './config'; -import { SinonSpy } from 'sinon'; +} from "./config"; +import { SinonSpy } from "sinon"; -const msgPayload = 'payload from test'; -const queueName = 'queue-name-from-unittest'; +const msgPayload = "payload from test"; +const queueName = "queue-name-from-unittest"; // signal that the channel is closed in test, thus it should not be closed again in afterEach. // could not find a way to get this from amqplib directly. const CHANNEL_CLOSED_IN_TEST = Symbol( - 'opentelemetry.amqplib.unittest.channel_closed_in_test' + "opentelemetry.amqplib.unittest.channel_closed_in_test" ); -describe('amqplib instrumentation promise model', () => { +describe("amqplib instrumentation promise model", () => { let conn: amqp.Connection; before(async () => { conn = await amqp.connect(rabbitMqUrl); @@ -94,7 +94,7 @@ describe('amqplib instrumentation promise model', () => { ); }; - describe('channel', () => { + describe("channel", () => { let channel: amqp.Channel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; beforeEach(async () => { endHookSpy = sinon.spy(); @@ -107,20 +107,20 @@ describe('amqplib instrumentation promise model', () => { await channel.purgeQueue(queueName); // install an error handler, otherwise when we have tests that create error on the channel, // it throws and crash process - channel.on('error', (err: Error) => {}); + channel.on("error", (err: Error) => {}); }); afterEach(async () => { if (!channel[CHANNEL_CLOSED_IN_TEST]) { try { - await new Promise(resolve => { - channel.on('close', resolve); + await new Promise((resolve) => { + channel.on("close", resolve); channel.close(); }); } catch {} } }); - it('simple publish and consume from queue', async () => { + it("simple publish and consume from queue", async () => { const hadSpaceInBuffer = channel.sendToQueue( queueName, Buffer.from(msgPayload) @@ -130,7 +130,7 @@ describe('amqplib instrumentation promise model', () => { await asyncConsume( channel, queueName, - [msg => expect(msg.content.toString()).toEqual(msgPayload)], + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -141,10 +141,10 @@ describe('amqplib instrumentation promise model', () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -155,10 +155,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(queueName); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -173,10 +173,10 @@ describe('amqplib instrumentation promise model', () => { expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -187,10 +187,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(queueName); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -212,23 +212,23 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - describe('ending consume spans', () => { - it('message acked sync', async () => { + describe("ending consume spans", () => { + it("message acked sync", async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); - await asyncConsume(channel, queueName, [msg => channel.ack(msg)]); + await asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]); // assert consumed message span has ended expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it('message acked async', async () => { + it("message acked async", async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); // start async timer and ack the message after the callback returns - await new Promise(resolve => { + await new Promise((resolve) => { asyncConsume(channel, queueName, [ - msg => + (msg) => setTimeout(() => { channel.ack(msg); resolve(); @@ -240,24 +240,24 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it('message nack no requeue', async () => { + it("message nack no requeue", async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); await asyncConsume(channel, queueName, [ - msg => channel.nack(msg, false, false), + (msg) => channel.nack(msg, false, false), ]); - await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again + await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again // assert consumed message span has ended expect(getTestSpans().length).toBe(2); const [_, consumerSpan] = getTestSpans(); expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(consumerSpan.status.message).toEqual( - 'nack called on message without requeue' + "nack called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Nack]); }); - it('message nack requeue, then acked', async () => { + it("message nack requeue, then acked", async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); await asyncConsume(channel, queueName, [ @@ -269,21 +269,21 @@ describe('amqplib instrumentation promise model', () => { const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(rejectedConsumerSpan.status.message).toEqual( - 'nack called on message with requeue' + "nack called on message with requeue" ); expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); }); - it('ack allUpTo 2 msgs sync', async () => { + it("ack allUpTo 2 msgs sync", async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ null, - msg => channel.ack(msg, true), - msg => channel.ack(msg), + (msg) => channel.ack(msg, true), + (msg) => channel.ack(msg), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); @@ -294,22 +294,22 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('nack allUpTo 2 msgs sync', async () => { + it("nack allUpTo 2 msgs sync", async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ null, - msg => channel.nack(msg, true, false), - msg => channel.nack(msg, false, false), + (msg) => channel.nack(msg, true, false), + (msg) => channel.nack(msg, false, false), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach(i => { + lodash.range(3, 6).forEach((i) => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - 'nack called on message without requeue' + "nack called on message without requeue" ); }); expectConsumeEndSpyStatus([ @@ -319,7 +319,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('ack not in received order', async () => { + it("ack not in received order", async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -337,7 +337,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('ackAll', async () => { + it("ackAll", async () => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -348,7 +348,7 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); }); - it('nackAll', async () => { + it("nackAll", async () => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -359,60 +359,60 @@ describe('amqplib instrumentation promise model', () => { ]); // assert all 2 span messages are ended by calling nackAll expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach(i => { + lodash.range(2, 4).forEach((i) => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - 'nackAll called on message without requeue' + "nackAll called on message without requeue" ); }); expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); }); - it('reject', async () => { + it("reject", async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ - msg => channel.reject(msg, false), + (msg) => channel.reject(msg, false), ]); expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - 'reject called on message without requeue' + "reject called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Reject]); }); - it('reject with requeue', async () => { + it("reject with requeue", async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ - msg => channel.reject(msg, true), - msg => channel.reject(msg, false), + (msg) => channel.reject(msg, true), + (msg) => channel.reject(msg, false), ]); expect(getTestSpans().length).toBe(3); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - 'reject called on message with requeue' + "reject called on message with requeue" ); expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[2].status.message).toEqual( - 'reject called on message without requeue' + "reject called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); }); - it('closing channel should end all open spans on it', async () => { + it("closing channel should end all open spans on it", async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); - await new Promise(resolve => + await new Promise((resolve) => asyncConsume(channel, queueName, [ - async msg => { + async (msg) => { await channel.close(); resolve(); channel[CHANNEL_CLOSED_IN_TEST] = true; @@ -422,22 +422,22 @@ describe('amqplib instrumentation promise model', () => { expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expect(getTestSpans()[1].status.message).toEqual("channel closed"); expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); }); - it('error on channel should end all open spans on it', done => { + it("error on channel should end all open spans on it", (done) => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); - channel.on('close', () => { + channel.on("close", () => { expect(getTestSpans().length).toBe(4); // second consume ended with valid ack, previous message not acked when channel is errored. // since we first ack the second message, it appear first in the finished spans array expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual('channel error'); + expect(getTestSpans()[3].status.message).toEqual("channel error"); expectConsumeEndSpyStatus([ EndOperation.Ack, EndOperation.ChannelError, @@ -446,7 +446,7 @@ describe('amqplib instrumentation promise model', () => { }); asyncConsume(channel, queueName, [ null, - msg => { + (msg) => { try { channel.ack(msg); channel[CHANNEL_CLOSED_IN_TEST] = true; @@ -457,7 +457,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('not acking the message trigger timeout', async () => { + it("not acking the message trigger timeout", async () => { instrumentation.setConfig({ consumeEndHook: endHookSpy, consumeTimeoutMs: 1, @@ -470,23 +470,23 @@ describe('amqplib instrumentation promise model', () => { await asyncConsume(channel, queueName, [null]); // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); }); }); - describe('routing and exchange', () => { - it('topic exchange', async () => { - const exchangeName = 'topic exchange'; - const routingKey = 'topic.name.from.unittest'; - await channel.assertExchange(exchangeName, 'topic', { durable: false }); + describe("routing and exchange", () => { + it("topic exchange", async () => { + const exchangeName = "topic exchange"; + const routingKey = "topic.name.from.unittest"; + await channel.assertExchange(exchangeName, "topic", { durable: false }); - const { queue: queueName } = await channel.assertQueue('', { + const { queue: queueName } = await channel.assertQueue("", { durable: false, }); - await channel.bindQueue(queueName, exchangeName, '#'); + await channel.bindQueue(queueName, exchangeName, "#"); channel.publish(exchangeName, routingKey, Buffer.from(msgPayload)); @@ -500,7 +500,7 @@ describe('amqplib instrumentation promise model', () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -514,16 +514,16 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(routingKey); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); // assert consume span expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -537,10 +537,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(routingKey); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); // assert context propagation expect(consumeSpan.spanContext().traceId).toEqual( @@ -552,29 +552,29 @@ describe('amqplib instrumentation promise model', () => { }); }); - describe('hooks', () => { - it('publish and consume hooks success', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; - const attributeNameFromEndHook = 'attribute.name.from.endhook'; - const endHookAttributeValue = 'attribute value from end hook'; + describe("hooks", () => { + it("publish and consume hooks success", async () => { + const attributeNameFromHook = "attribute.name.from.hook"; + const hookAttributeValue = "attribute value from hook"; + const attributeNameFromEndHook = "attribute.name.from.endhook"; + const endHookAttributeValue = "attribute value from end hook"; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(publishParams.exchange).toEqual(''); + expect(publishParams.exchange).toEqual(""); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); + span.setAttribute(attributeNameFromHook, hookAttributeValue); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); expect(consumeInfo.msg!.content.toString()).toEqual(msgPayload); + span.setAttribute(attributeNameFromHook, hookAttributeValue); }, consumeEndHook: ( span: Span, consumeEndInfo: ConsumeEndInfo ): void => { - span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); expect(consumeEndInfo.endOperation).toEqual(EndOperation.AutoAck); + span.setAttribute(attributeNameFromEndHook, endHookAttributeValue); }, }); @@ -595,17 +595,17 @@ describe('amqplib instrumentation promise model', () => { ); }); - it('hooks throw should not affect user flow or span creation', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; + it("hooks throw should not affect user flow or span creation", async () => { + const attributeNameFromHook = "attribute.name.from.hook"; + const hookAttributeValue = "attribute value from hook"; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); + throw new Error("error from hook"); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); + throw new Error("error from hook"); }, }); @@ -615,7 +615,7 @@ describe('amqplib instrumentation promise model', () => { noAck: true, }); expect(getTestSpans().length).toBe(2); - getTestSpans().forEach(s => + getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual( hookAttributeValue ) @@ -623,9 +623,9 @@ describe('amqplib instrumentation promise model', () => { }); }); - describe('delete queue', () => { - it('consumer receives null msg when a queue is deleted in broker', async () => { - const queueNameForDeletion = 'queue-to-be-deleted'; + describe("delete queue", () => { + it("consumer receives null msg when a queue is deleted in broker", async () => { + const queueNameForDeletion = "queue-to-be-deleted"; await channel.assertQueue(queueNameForDeletion, { durable: false }); await channel.purgeQueue(queueNameForDeletion); @@ -639,7 +639,7 @@ describe('amqplib instrumentation promise model', () => { }); }); - describe('confirm channel', () => { + describe("confirm channel", () => { let confirmChannel: amqp.ConfirmChannel & { [CHANNEL_CLOSED_IN_TEST]?: boolean; }; @@ -654,26 +654,26 @@ describe('amqplib instrumentation promise model', () => { await confirmChannel.purgeQueue(queueName); // install an error handler, otherwise when we have tests that create error on the channel, // it throws and crash process - confirmChannel.on('error', (err: Error) => {}); + confirmChannel.on("error", (err: Error) => {}); }); afterEach(async () => { if (!confirmChannel[CHANNEL_CLOSED_IN_TEST]) { try { - await new Promise(resolve => { - confirmChannel.on('close', resolve); + await new Promise((resolve) => { + confirmChannel.on("close", resolve); confirmChannel.close(); }); } catch {} } }); - it('simple publish with confirm and consume from queue', async () => { + it("simple publish with confirm and consume from queue", async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume( confirmChannel, queueName, - [msg => expect(msg.content.toString()).toEqual(msgPayload)], + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -684,10 +684,10 @@ describe('amqplib instrumentation promise model', () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -698,10 +698,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(queueName); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -716,10 +716,10 @@ describe('amqplib instrumentation promise model', () => { expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -730,10 +730,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(queueName); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -752,16 +752,16 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - it('confirm throw should not affect span end', async () => { - const confirmUserError = new Error('callback error'); + it("confirm throw should not affect span end", async () => { + const confirmUserError = new Error("callback error"); await asyncConfirmSend(confirmChannel, queueName, msgPayload, () => { throw confirmUserError; - }).catch(reject => expect(reject).toEqual(confirmUserError)); + }).catch((reject) => expect(reject).toEqual(confirmUserError)); await asyncConsume( confirmChannel, queueName, - [msg => expect(msg.content.toString()).toEqual(msgPayload)], + [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -771,25 +771,25 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - describe('ending consume spans', () => { - it('message acked sync', async () => { + describe("ending consume spans", () => { + it("message acked sync", async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ - msg => confirmChannel.ack(msg), + (msg) => confirmChannel.ack(msg), ]); // assert consumed message span has ended expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it('message acked async', async () => { + it("message acked async", async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); // start async timer and ack the message after the callback returns - await new Promise(resolve => { + await new Promise((resolve) => { asyncConsume(confirmChannel, queueName, [ - msg => + (msg) => setTimeout(() => { confirmChannel.ack(msg); resolve(); @@ -801,24 +801,24 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it('message nack no requeue', async () => { + it("message nack no requeue", async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ - msg => confirmChannel.nack(msg, false, false), + (msg) => confirmChannel.nack(msg, false, false), ]); - await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again + await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again // assert consumed message span has ended expect(getTestSpans().length).toBe(2); const [_, consumerSpan] = getTestSpans(); expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(consumerSpan.status.message).toEqual( - 'nack called on message without requeue' + "nack called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Nack]); }); - it('message nack requeue, then acked', async () => { + it("message nack requeue, then acked", async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ @@ -830,13 +830,13 @@ describe('amqplib instrumentation promise model', () => { const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(rejectedConsumerSpan.status.message).toEqual( - 'nack called on message with requeue' + "nack called on message with requeue" ); expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); }); - it('ack allUpTo 2 msgs sync', async () => { + it("ack allUpTo 2 msgs sync", async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -845,8 +845,8 @@ describe('amqplib instrumentation promise model', () => { await asyncConsume(confirmChannel, queueName, [ null, - msg => confirmChannel.ack(msg, true), - msg => confirmChannel.ack(msg), + (msg) => confirmChannel.ack(msg, true), + (msg) => confirmChannel.ack(msg), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); @@ -857,7 +857,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('nack allUpTo 2 msgs sync', async () => { + it("nack allUpTo 2 msgs sync", async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -866,15 +866,15 @@ describe('amqplib instrumentation promise model', () => { await asyncConsume(confirmChannel, queueName, [ null, - msg => confirmChannel.nack(msg, true, false), - msg => confirmChannel.nack(msg, false, false), + (msg) => confirmChannel.nack(msg, true, false), + (msg) => confirmChannel.nack(msg, false, false), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach(i => { + lodash.range(3, 6).forEach((i) => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - 'nack called on message without requeue' + "nack called on message without requeue" ); }); expectConsumeEndSpyStatus([ @@ -884,7 +884,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('ack not in received order', async () => { + it("ack not in received order", async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -908,7 +908,7 @@ describe('amqplib instrumentation promise model', () => { ]); }); - it('ackAll', async () => { + it("ackAll", async () => { await Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -924,7 +924,7 @@ describe('amqplib instrumentation promise model', () => { expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); }); - it('nackAll', async () => { + it("nackAll", async () => { await Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -937,16 +937,16 @@ describe('amqplib instrumentation promise model', () => { ]); // assert all 2 span messages are ended by calling nackAll expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach(i => { + lodash.range(2, 4).forEach((i) => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - 'nackAll called on message without requeue' + "nackAll called on message without requeue" ); }); expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); }); - it('reject', async () => { + it("reject", async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -954,17 +954,17 @@ describe('amqplib instrumentation promise model', () => { ); await asyncConsume(confirmChannel, queueName, [ - msg => confirmChannel.reject(msg, false), + (msg) => confirmChannel.reject(msg, false), ]); expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - 'reject called on message without requeue' + "reject called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Reject]); }); - it('reject with requeue', async () => { + it("reject with requeue", async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -972,31 +972,31 @@ describe('amqplib instrumentation promise model', () => { ); await asyncConsume(confirmChannel, queueName, [ - msg => confirmChannel.reject(msg, true), - msg => confirmChannel.reject(msg, false), + (msg) => confirmChannel.reject(msg, true), + (msg) => confirmChannel.reject(msg, false), ]); expect(getTestSpans().length).toBe(3); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - 'reject called on message with requeue' + "reject called on message with requeue" ); expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[2].status.message).toEqual( - 'reject called on message without requeue' + "reject called on message without requeue" ); expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); }); - it('closing channel should end all open spans on it', async () => { + it("closing channel should end all open spans on it", async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) ) ); - await new Promise(resolve => + await new Promise((resolve) => asyncConsume(confirmChannel, queueName, [ - async msg => { + async (msg) => { await confirmChannel.close(); resolve(); confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; @@ -1006,23 +1006,23 @@ describe('amqplib instrumentation promise model', () => { expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual('channel closed'); + expect(getTestSpans()[1].status.message).toEqual("channel closed"); expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); }); - it('error on channel should end all open spans on it', done => { + it("error on channel should end all open spans on it", (done) => { Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) ) ).then(() => { - confirmChannel.on('close', () => { + confirmChannel.on("close", () => { expect(getTestSpans().length).toBe(4); // second consume ended with valid ack, previous message not acked when channel is errored. // since we first ack the second message, it appear first in the finished spans array expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual('channel error'); + expect(getTestSpans()[3].status.message).toEqual("channel error"); expectConsumeEndSpyStatus([ EndOperation.Ack, EndOperation.ChannelError, @@ -1031,7 +1031,7 @@ describe('amqplib instrumentation promise model', () => { }); asyncConsume(confirmChannel, queueName, [ null, - msg => { + (msg) => { try { confirmChannel.ack(msg); confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; @@ -1043,7 +1043,7 @@ describe('amqplib instrumentation promise model', () => { }); }); - it('not acking the message trigger timeout', async () => { + it("not acking the message trigger timeout", async () => { instrumentation.setConfig({ consumeEndHook: endHookSpy, consumeTimeoutMs: 1, @@ -1058,25 +1058,25 @@ describe('amqplib instrumentation promise model', () => { await asyncConsume(confirmChannel, queueName, [null]); // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); }); }); - describe('routing and exchange', () => { - it('topic exchange', async () => { - const exchangeName = 'topic exchange'; - const routingKey = 'topic.name.from.unittest'; - await confirmChannel.assertExchange(exchangeName, 'topic', { + describe("routing and exchange", () => { + it("topic exchange", async () => { + const exchangeName = "topic exchange"; + const routingKey = "topic.name.from.unittest"; + await confirmChannel.assertExchange(exchangeName, "topic", { durable: false, }); - const { queue: queueName } = await confirmChannel.assertQueue('', { + const { queue: queueName } = await confirmChannel.assertQueue("", { durable: false, }); - await confirmChannel.bindQueue(queueName, exchangeName, '#'); + await confirmChannel.bindQueue(queueName, exchangeName, "#"); await asyncConfirmPublish( confirmChannel, @@ -1095,7 +1095,7 @@ describe('amqplib instrumentation promise model', () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -1109,16 +1109,16 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(routingKey); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); // assert consume span expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual('rabbitmq'); + ).toEqual("rabbitmq"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -1132,10 +1132,10 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(routingKey); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual('AMQP'); + ).toEqual("AMQP"); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual('0.9.1'); + ).toEqual("0.9.1"); // assert context propagation expect(consumeSpan.spanContext().traceId).toEqual( @@ -1147,38 +1147,38 @@ describe('amqplib instrumentation promise model', () => { }); }); - describe('hooks', () => { - it('publish and consume hooks success', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; + describe("hooks", () => { + it("publish and consume hooks success", async () => { + const attributeNameFromHook = "attribute.name.from.hook"; + const hookAttributeValue = "attribute value from hook"; const attributeNameFromConfirmEndHook = - 'attribute.name.from.confirm.endhook'; + "attribute.name.from.confirm.endhook"; const confirmEndHookAttributeValue = - 'attribute value from confirm end hook'; + "attribute value from confirm end hook"; const attributeNameFromConsumeEndHook = - 'attribute.name.from.consume.endhook'; + "attribute.name.from.consume.endhook"; const consumeEndHookAttributeValue = - 'attribute value from consume end hook'; + "attribute value from consume end hook"; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo) => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); - expect(publishParams.exchange).toEqual(''); + expect(publishParams.exchange).toEqual(""); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); expect(publishParams.isConfirmChannel).toBe(true); + span.setAttribute(attributeNameFromHook, hookAttributeValue); }, publishConfirmHook: (span, publishParams) => { + expect(publishParams.exchange).toEqual(""); + expect(publishParams.routingKey).toEqual(queueName); + expect(publishParams.content.toString()).toEqual(msgPayload); span.setAttribute( attributeNameFromConfirmEndHook, confirmEndHookAttributeValue ); - expect(publishParams.exchange).toEqual(''); - expect(publishParams.routingKey).toEqual(queueName); - expect(publishParams.content.toString()).toEqual(msgPayload); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo) => { - span.setAttribute(attributeNameFromHook, hookAttributeValue); expect(consumeInfo.msg!.content.toString()).toEqual(msgPayload); + span.setAttribute(attributeNameFromHook, hookAttributeValue); }, consumeEndHook: ( span: Span, @@ -1212,24 +1212,24 @@ describe('amqplib instrumentation promise model', () => { ).toEqual(consumeEndHookAttributeValue); }); - it('hooks throw should not affect user flow or span creation', async () => { - const attributeNameFromHook = 'attribute.name.from.hook'; - const hookAttributeValue = 'attribute value from hook'; + it("hooks throw should not affect user flow or span creation", async () => { + const attributeNameFromHook = "attribute.name.from.hook"; + const hookAttributeValue = "attribute value from hook"; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); + throw new Error("error from hook"); }, publishConfirmHook: ( span: Span, publishParams: PublishInfo ): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); + throw new Error("error from hook"); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error('error from hook'); + throw new Error("error from hook"); }, }); @@ -1239,7 +1239,7 @@ describe('amqplib instrumentation promise model', () => { noAck: true, }); expect(getTestSpans().length).toBe(2); - getTestSpans().forEach(s => + getTestSpans().forEach((s) => expect(s.attributes[attributeNameFromHook]).toEqual( hookAttributeValue ) From 352d5e65971ffbd20e55bdd46f0721cabf36ee86 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 13 Feb 2022 18:18:37 +0200 Subject: [PATCH 23/39] fix(amqplib): used modified options for hook --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index a71b3be2c4..1d4bcd9742 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -501,7 +501,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { exchange, routingKey, content, - options, + options: modifiedOptions, isConfirmChannel: true, }), e => { @@ -601,7 +601,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { exchange, routingKey, content, - options, + options: modifiedOptions, isConfirmChannel: false, }), e => { From 783dd52fe65e431a53a35ea21b0643159d00ed7d Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 09:56:23 +0200 Subject: [PATCH 24/39] chore(amqplib): fill copyright template --- plugins/node/instrumentation-amqplib/LICENSE | 2 +- plugins/node/instrumentation-amqplib/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/LICENSE b/plugins/node/instrumentation-amqplib/LICENSE index 261eeb9e9f..a62e729e4a 100644 --- a/plugins/node/instrumentation-amqplib/LICENSE +++ b/plugins/node/instrumentation-amqplib/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + [2022] OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json index c8d9320cec..915d1c01e6 100644 --- a/plugins/node/instrumentation-amqplib/package.json +++ b/plugins/node/instrumentation-amqplib/package.json @@ -1,5 +1,5 @@ { - "name": "@opentelemetry/instrumentation-amqplib", + "name": "@mzahor-test-org/instrumentation-amqplib", "version": "0.27.0", "description": "OpenTelemetry automatic instrumentation for the `amqplib` package", "keywords": [ From 262b2da374b882754899f573f5afceb59b4a9afc Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 10:45:51 +0200 Subject: [PATCH 25/39] fix(amqplib): check error with == null --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 1d4bcd9742..3150954a5c 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -241,7 +241,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { url, socketOptions, function (this: unknown, err, conn: amqp.Connection) { - if (err === null) { + if (err == null) { const urlAttributes = getConnectionAttributesFromUrl(url); // the type of conn in @types/amqplib is amqp.Connection, but in practice the library send the // `serverProperties` on the `conn` and not in a property `connection`. From 30bb3c8b6c95006be43af4b7072fba3c480e0c7b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 10:49:09 +0200 Subject: [PATCH 26/39] refactor(amqplib): set to undefined instead of delete --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 3150954a5c..3d85f7e802 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -279,7 +279,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { if (activeTimer) { clearInterval(activeTimer); } - delete this[CHANNEL_CONSUME_TIMEOUT_TIMER]; + this[CHANNEL_CONSUME_TIMEOUT_TIMER] = undefined; } else if (eventName === 'error') { self.endAllSpansOnChannel( this, @@ -686,7 +686,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { } this.callConsumeEndHook(storedSpan, message, isRejected, operation); storedSpan.end(); - delete message[MESSAGE_STORED_SPAN]; + message[MESSAGE_STORED_SPAN] = undefined; } private endAllSpansOnChannel( From c340816ded9fc304b064e748866fa9f8d91bf890 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 11:23:49 +0200 Subject: [PATCH 27/39] refactor(amqplib): don't use defineProperty for symbols --- .../instrumentation-amqplib/src/amqplib.ts | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 3d85f7e802..36643c9b8c 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -251,10 +251,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { const serverAttributes = getConnectionAttributesFromServer( conn as any ); - Object.defineProperty(conn, CONNECTION_ATTRIBUTES, { - value: { ...urlAttributes, ...serverAttributes }, - enumerable: false, - }); + (conn as any)[CONNECTION_ATTRIBUTES] = { ...urlAttributes, ...serverAttributes }; } openCallback.apply(this, arguments); } @@ -381,22 +378,14 @@ export class AmqplibInstrumentation extends InstrumentationBase { self.checkConsumeTimeoutOnChannel(channel); }, self._config.consumeTimeoutMs); timer.unref(); - Object.defineProperty(channel, CHANNEL_CONSUME_TIMEOUT_TIMER, { - value: timer, - enumerable: false, - configurable: true, - }); + channel[CHANNEL_CONSUME_TIMEOUT_TIMER] = timer; } - Object.defineProperty(channel, CHANNEL_SPANS_NOT_ENDED, { - value: [], - enumerable: false, - configurable: true, - }); + channel[CHANNEL_SPANS_NOT_ENDED] = []; } const patchedOnMessage = function ( this: unknown, - msg: amqp.ConsumeMessage | null + msg: InstrumentationMessage | null ) { // msg is expected to be null for signaling consumer cancel notification // https://www.rabbitmq.com/consumer-cancel.html @@ -450,11 +439,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { }); // store the span on the message, so we can end it when user call 'ack' on it - Object.defineProperty(msg, MESSAGE_STORED_SPAN, { - value: span, - enumerable: false, - configurable: true, - }); + msg[MESSAGE_STORED_SPAN] = span; } context.with(trace.setSpan(context.active(), span), () => { @@ -700,11 +685,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { spansNotEnded.forEach(msgDetails => { this.endConsumerSpan(msgDetails.msg, isRejected, operation, requeue); }); - Object.defineProperty(channel, CHANNEL_SPANS_NOT_ENDED, { - value: [], - enumerable: false, - configurable: true, - }); + channel[CHANNEL_SPANS_NOT_ENDED] = []; } private callConsumeEndHook( From 73b793590d72fa1156900574c8438c0a9055804a Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 11:24:27 +0200 Subject: [PATCH 28/39] revert(amqplib): revert package name change for testing --- plugins/node/instrumentation-amqplib/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json index 915d1c01e6..c8d9320cec 100644 --- a/plugins/node/instrumentation-amqplib/package.json +++ b/plugins/node/instrumentation-amqplib/package.json @@ -1,5 +1,5 @@ { - "name": "@mzahor-test-org/instrumentation-amqplib", + "name": "@opentelemetry/instrumentation-amqplib", "version": "0.27.0", "description": "OpenTelemetry automatic instrumentation for the `amqplib` package", "keywords": [ From dca55046d5bfba9b6cad5329a54df37cb968d5fd Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 11:27:39 +0200 Subject: [PATCH 29/39] chore(amqplib): lint fix --- .../instrumentation-amqplib/src/amqplib.ts | 5 +- .../test/amqplib-promise.test.ts | 388 +++++++++--------- 2 files changed, 198 insertions(+), 195 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 36643c9b8c..5abc1c4a8e 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -251,7 +251,10 @@ export class AmqplibInstrumentation extends InstrumentationBase { const serverAttributes = getConnectionAttributesFromServer( conn as any ); - (conn as any)[CONNECTION_ATTRIBUTES] = { ...urlAttributes, ...serverAttributes }; + (conn as any)[CONNECTION_ATTRIBUTES] = { + ...urlAttributes, + ...serverAttributes, + }; } openCallback.apply(this, arguments); } diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts index 8fb872219b..4863881739 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-promise.test.ts @@ -13,52 +13,52 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import "mocha"; -import * as expect from "expect"; -import * as sinon from "sinon"; -import * as lodash from "lodash"; +import 'mocha'; +import * as expect from 'expect'; +import * as sinon from 'sinon'; +import * as lodash from 'lodash'; import { AmqplibInstrumentation, ConsumeEndInfo, ConsumeInfo, EndOperation, PublishInfo, -} from "../src"; +} from '../src'; import { getTestSpans, registerInstrumentationTesting, -} from "@opentelemetry/contrib-test-utils"; +} from '@opentelemetry/contrib-test-utils'; const instrumentation = registerInstrumentationTesting( new AmqplibInstrumentation() ); -import * as amqp from "amqplib"; -import { ConsumeMessage } from "amqplib"; +import * as amqp from 'amqplib'; +import { ConsumeMessage } from 'amqplib'; import { MessagingDestinationKindValues, SemanticAttributes, -} from "@opentelemetry/semantic-conventions"; -import { Span, SpanKind, SpanStatusCode } from "@opentelemetry/api"; -import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from "./utils"; +} from '@opentelemetry/semantic-conventions'; +import { Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { asyncConfirmPublish, asyncConfirmSend, asyncConsume } from './utils'; import { censoredUrl, rabbitMqUrl, TEST_RABBITMQ_HOST, TEST_RABBITMQ_PORT, -} from "./config"; -import { SinonSpy } from "sinon"; +} from './config'; +import { SinonSpy } from 'sinon'; -const msgPayload = "payload from test"; -const queueName = "queue-name-from-unittest"; +const msgPayload = 'payload from test'; +const queueName = 'queue-name-from-unittest'; // signal that the channel is closed in test, thus it should not be closed again in afterEach. // could not find a way to get this from amqplib directly. const CHANNEL_CLOSED_IN_TEST = Symbol( - "opentelemetry.amqplib.unittest.channel_closed_in_test" + 'opentelemetry.amqplib.unittest.channel_closed_in_test' ); -describe("amqplib instrumentation promise model", () => { +describe('amqplib instrumentation promise model', () => { let conn: amqp.Connection; before(async () => { conn = await amqp.connect(rabbitMqUrl); @@ -94,7 +94,7 @@ describe("amqplib instrumentation promise model", () => { ); }; - describe("channel", () => { + describe('channel', () => { let channel: amqp.Channel & { [CHANNEL_CLOSED_IN_TEST]?: boolean }; beforeEach(async () => { endHookSpy = sinon.spy(); @@ -107,20 +107,20 @@ describe("amqplib instrumentation promise model", () => { await channel.purgeQueue(queueName); // install an error handler, otherwise when we have tests that create error on the channel, // it throws and crash process - channel.on("error", (err: Error) => {}); + channel.on('error', (err: Error) => {}); }); afterEach(async () => { if (!channel[CHANNEL_CLOSED_IN_TEST]) { try { - await new Promise((resolve) => { - channel.on("close", resolve); + await new Promise(resolve => { + channel.on('close', resolve); channel.close(); }); } catch {} } }); - it("simple publish and consume from queue", async () => { + it('simple publish and consume from queue', async () => { const hadSpaceInBuffer = channel.sendToQueue( queueName, Buffer.from(msgPayload) @@ -130,7 +130,7 @@ describe("amqplib instrumentation promise model", () => { await asyncConsume( channel, queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + [msg => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -141,10 +141,10 @@ describe("amqplib instrumentation promise model", () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -155,10 +155,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(queueName); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -173,10 +173,10 @@ describe("amqplib instrumentation promise model", () => { expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -187,10 +187,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(queueName); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -212,23 +212,23 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - describe("ending consume spans", () => { - it("message acked sync", async () => { + describe('ending consume spans', () => { + it('message acked sync', async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); - await asyncConsume(channel, queueName, [(msg) => channel.ack(msg)]); + await asyncConsume(channel, queueName, [msg => channel.ack(msg)]); // assert consumed message span has ended expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it("message acked async", async () => { + it('message acked async', async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); // start async timer and ack the message after the callback returns - await new Promise((resolve) => { + await new Promise(resolve => { asyncConsume(channel, queueName, [ - (msg) => + msg => setTimeout(() => { channel.ack(msg); resolve(); @@ -240,24 +240,24 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it("message nack no requeue", async () => { + it('message nack no requeue', async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); await asyncConsume(channel, queueName, [ - (msg) => channel.nack(msg, false, false), + msg => channel.nack(msg, false, false), ]); - await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again + await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again // assert consumed message span has ended expect(getTestSpans().length).toBe(2); const [_, consumerSpan] = getTestSpans(); expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(consumerSpan.status.message).toEqual( - "nack called on message without requeue" + 'nack called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Nack]); }); - it("message nack requeue, then acked", async () => { + it('message nack requeue, then acked', async () => { channel.sendToQueue(queueName, Buffer.from(msgPayload)); await asyncConsume(channel, queueName, [ @@ -269,21 +269,21 @@ describe("amqplib instrumentation promise model", () => { const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(rejectedConsumerSpan.status.message).toEqual( - "nack called on message with requeue" + 'nack called on message with requeue' ); expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); }); - it("ack allUpTo 2 msgs sync", async () => { + it('ack allUpTo 2 msgs sync', async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ null, - (msg) => channel.ack(msg, true), - (msg) => channel.ack(msg), + msg => channel.ack(msg, true), + msg => channel.ack(msg), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); @@ -294,22 +294,22 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("nack allUpTo 2 msgs sync", async () => { + it('nack allUpTo 2 msgs sync', async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ null, - (msg) => channel.nack(msg, true, false), - (msg) => channel.nack(msg, false, false), + msg => channel.nack(msg, true, false), + msg => channel.nack(msg, false, false), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach((i) => { + lodash.range(3, 6).forEach(i => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - "nack called on message without requeue" + 'nack called on message without requeue' ); }); expectConsumeEndSpyStatus([ @@ -319,7 +319,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("ack not in received order", async () => { + it('ack not in received order', async () => { lodash.times(3, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -337,7 +337,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("ackAll", async () => { + it('ackAll', async () => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -348,7 +348,7 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); }); - it("nackAll", async () => { + it('nackAll', async () => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); @@ -359,60 +359,60 @@ describe("amqplib instrumentation promise model", () => { ]); // assert all 2 span messages are ended by calling nackAll expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach((i) => { + lodash.range(2, 4).forEach(i => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - "nackAll called on message without requeue" + 'nackAll called on message without requeue' ); }); expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); }); - it("reject", async () => { + it('reject', async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ - (msg) => channel.reject(msg, false), + msg => channel.reject(msg, false), ]); expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - "reject called on message without requeue" + 'reject called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Reject]); }); - it("reject with requeue", async () => { + it('reject with requeue', async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); await asyncConsume(channel, queueName, [ - (msg) => channel.reject(msg, true), - (msg) => channel.reject(msg, false), + msg => channel.reject(msg, true), + msg => channel.reject(msg, false), ]); expect(getTestSpans().length).toBe(3); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - "reject called on message with requeue" + 'reject called on message with requeue' ); expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[2].status.message).toEqual( - "reject called on message without requeue" + 'reject called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); }); - it("closing channel should end all open spans on it", async () => { + it('closing channel should end all open spans on it', async () => { lodash.times(1, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); - await new Promise((resolve) => + await new Promise(resolve => asyncConsume(channel, queueName, [ - async (msg) => { + async msg => { await channel.close(); resolve(); channel[CHANNEL_CLOSED_IN_TEST] = true; @@ -422,22 +422,22 @@ describe("amqplib instrumentation promise model", () => { expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual("channel closed"); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); }); - it("error on channel should end all open spans on it", (done) => { + it('error on channel should end all open spans on it', done => { lodash.times(2, () => channel.sendToQueue(queueName, Buffer.from(msgPayload)) ); - channel.on("close", () => { + channel.on('close', () => { expect(getTestSpans().length).toBe(4); // second consume ended with valid ack, previous message not acked when channel is errored. // since we first ack the second message, it appear first in the finished spans array expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual("channel error"); + expect(getTestSpans()[3].status.message).toEqual('channel error'); expectConsumeEndSpyStatus([ EndOperation.Ack, EndOperation.ChannelError, @@ -446,7 +446,7 @@ describe("amqplib instrumentation promise model", () => { }); asyncConsume(channel, queueName, [ null, - (msg) => { + msg => { try { channel.ack(msg); channel[CHANNEL_CLOSED_IN_TEST] = true; @@ -457,7 +457,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("not acking the message trigger timeout", async () => { + it('not acking the message trigger timeout', async () => { instrumentation.setConfig({ consumeEndHook: endHookSpy, consumeTimeoutMs: 1, @@ -470,23 +470,23 @@ describe("amqplib instrumentation promise model", () => { await asyncConsume(channel, queueName, [null]); // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 10)); expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); }); }); - describe("routing and exchange", () => { - it("topic exchange", async () => { - const exchangeName = "topic exchange"; - const routingKey = "topic.name.from.unittest"; - await channel.assertExchange(exchangeName, "topic", { durable: false }); + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await channel.assertExchange(exchangeName, 'topic', { durable: false }); - const { queue: queueName } = await channel.assertQueue("", { + const { queue: queueName } = await channel.assertQueue('', { durable: false, }); - await channel.bindQueue(queueName, exchangeName, "#"); + await channel.bindQueue(queueName, exchangeName, '#'); channel.publish(exchangeName, routingKey, Buffer.from(msgPayload)); @@ -500,7 +500,7 @@ describe("amqplib instrumentation promise model", () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -514,16 +514,16 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(routingKey); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); // assert consume span expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -537,10 +537,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(routingKey); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); // assert context propagation expect(consumeSpan.spanContext().traceId).toEqual( @@ -552,15 +552,15 @@ describe("amqplib instrumentation promise model", () => { }); }); - describe("hooks", () => { - it("publish and consume hooks success", async () => { - const attributeNameFromHook = "attribute.name.from.hook"; - const hookAttributeValue = "attribute value from hook"; - const attributeNameFromEndHook = "attribute.name.from.endhook"; - const endHookAttributeValue = "attribute value from end hook"; + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; + const attributeNameFromEndHook = 'attribute.name.from.endhook'; + const endHookAttributeValue = 'attribute value from end hook'; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { - expect(publishParams.exchange).toEqual(""); + expect(publishParams.exchange).toEqual(''); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); span.setAttribute(attributeNameFromHook, hookAttributeValue); @@ -595,17 +595,17 @@ describe("amqplib instrumentation promise model", () => { ); }); - it("hooks throw should not affect user flow or span creation", async () => { - const attributeNameFromHook = "attribute.name.from.hook"; - const hookAttributeValue = "attribute value from hook"; + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error("error from hook"); + throw new Error('error from hook'); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error("error from hook"); + throw new Error('error from hook'); }, }); @@ -615,7 +615,7 @@ describe("amqplib instrumentation promise model", () => { noAck: true, }); expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => + getTestSpans().forEach(s => expect(s.attributes[attributeNameFromHook]).toEqual( hookAttributeValue ) @@ -623,9 +623,9 @@ describe("amqplib instrumentation promise model", () => { }); }); - describe("delete queue", () => { - it("consumer receives null msg when a queue is deleted in broker", async () => { - const queueNameForDeletion = "queue-to-be-deleted"; + describe('delete queue', () => { + it('consumer receives null msg when a queue is deleted in broker', async () => { + const queueNameForDeletion = 'queue-to-be-deleted'; await channel.assertQueue(queueNameForDeletion, { durable: false }); await channel.purgeQueue(queueNameForDeletion); @@ -639,7 +639,7 @@ describe("amqplib instrumentation promise model", () => { }); }); - describe("confirm channel", () => { + describe('confirm channel', () => { let confirmChannel: amqp.ConfirmChannel & { [CHANNEL_CLOSED_IN_TEST]?: boolean; }; @@ -654,26 +654,26 @@ describe("amqplib instrumentation promise model", () => { await confirmChannel.purgeQueue(queueName); // install an error handler, otherwise when we have tests that create error on the channel, // it throws and crash process - confirmChannel.on("error", (err: Error) => {}); + confirmChannel.on('error', (err: Error) => {}); }); afterEach(async () => { if (!confirmChannel[CHANNEL_CLOSED_IN_TEST]) { try { - await new Promise((resolve) => { - confirmChannel.on("close", resolve); + await new Promise(resolve => { + confirmChannel.on('close', resolve); confirmChannel.close(); }); } catch {} } }); - it("simple publish with confirm and consume from queue", async () => { + it('simple publish with confirm and consume from queue', async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume( confirmChannel, queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + [msg => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -684,10 +684,10 @@ describe("amqplib instrumentation promise model", () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -698,10 +698,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(queueName); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); expect(publishSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -716,10 +716,10 @@ describe("amqplib instrumentation promise model", () => { expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] - ).toEqual(""); // according to spec: "This will be an empty string if the default exchange is used" + ).toEqual(''); // according to spec: "This will be an empty string if the default exchange is used" expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION_KIND] ).toEqual(MessagingDestinationKindValues.TOPIC); @@ -730,10 +730,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(queueName); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); expect(consumeSpan.attributes[SemanticAttributes.MESSAGING_URL]).toEqual( censoredUrl ); @@ -752,16 +752,16 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - it("confirm throw should not affect span end", async () => { - const confirmUserError = new Error("callback error"); + it('confirm throw should not affect span end', async () => { + const confirmUserError = new Error('callback error'); await asyncConfirmSend(confirmChannel, queueName, msgPayload, () => { throw confirmUserError; - }).catch((reject) => expect(reject).toEqual(confirmUserError)); + }).catch(reject => expect(reject).toEqual(confirmUserError)); await asyncConsume( confirmChannel, queueName, - [(msg) => expect(msg.content.toString()).toEqual(msgPayload)], + [msg => expect(msg.content.toString()).toEqual(msgPayload)], { noAck: true, } @@ -771,25 +771,25 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.AutoAck]); }); - describe("ending consume spans", () => { - it("message acked sync", async () => { + describe('ending consume spans', () => { + it('message acked sync', async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ - (msg) => confirmChannel.ack(msg), + msg => confirmChannel.ack(msg), ]); // assert consumed message span has ended expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it("message acked async", async () => { + it('message acked async', async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); // start async timer and ack the message after the callback returns - await new Promise((resolve) => { + await new Promise(resolve => { asyncConsume(confirmChannel, queueName, [ - (msg) => + msg => setTimeout(() => { confirmChannel.ack(msg); resolve(); @@ -801,24 +801,24 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.Ack]); }); - it("message nack no requeue", async () => { + it('message nack no requeue', async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ - (msg) => confirmChannel.nack(msg, false, false), + msg => confirmChannel.nack(msg, false, false), ]); - await new Promise((resolve) => setTimeout(resolve, 20)); // just make sure we don't get it again + await new Promise(resolve => setTimeout(resolve, 20)); // just make sure we don't get it again // assert consumed message span has ended expect(getTestSpans().length).toBe(2); const [_, consumerSpan] = getTestSpans(); expect(consumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(consumerSpan.status.message).toEqual( - "nack called on message without requeue" + 'nack called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Nack]); }); - it("message nack requeue, then acked", async () => { + it('message nack requeue, then acked', async () => { await asyncConfirmSend(confirmChannel, queueName, msgPayload); await asyncConsume(confirmChannel, queueName, [ @@ -830,13 +830,13 @@ describe("amqplib instrumentation promise model", () => { const [_, rejectedConsumerSpan, successConsumerSpan] = getTestSpans(); expect(rejectedConsumerSpan.status.code).toEqual(SpanStatusCode.ERROR); expect(rejectedConsumerSpan.status.message).toEqual( - "nack called on message with requeue" + 'nack called on message with requeue' ); expect(successConsumerSpan.status.code).toEqual(SpanStatusCode.UNSET); expectConsumeEndSpyStatus([EndOperation.Nack, EndOperation.Ack]); }); - it("ack allUpTo 2 msgs sync", async () => { + it('ack allUpTo 2 msgs sync', async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -845,8 +845,8 @@ describe("amqplib instrumentation promise model", () => { await asyncConsume(confirmChannel, queueName, [ null, - (msg) => confirmChannel.ack(msg, true), - (msg) => confirmChannel.ack(msg), + msg => confirmChannel.ack(msg, true), + msg => confirmChannel.ack(msg), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); @@ -857,7 +857,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("nack allUpTo 2 msgs sync", async () => { + it('nack allUpTo 2 msgs sync', async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -866,15 +866,15 @@ describe("amqplib instrumentation promise model", () => { await asyncConsume(confirmChannel, queueName, [ null, - (msg) => confirmChannel.nack(msg, true, false), - (msg) => confirmChannel.nack(msg, false, false), + msg => confirmChannel.nack(msg, true, false), + msg => confirmChannel.nack(msg, false, false), ]); // assert all 3 messages are acked, including the first one which is acked by allUpTo expect(getTestSpans().length).toBe(6); - lodash.range(3, 6).forEach((i) => { + lodash.range(3, 6).forEach(i => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - "nack called on message without requeue" + 'nack called on message without requeue' ); }); expectConsumeEndSpyStatus([ @@ -884,7 +884,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("ack not in received order", async () => { + it('ack not in received order', async () => { await Promise.all( lodash.times(3, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -908,7 +908,7 @@ describe("amqplib instrumentation promise model", () => { ]); }); - it("ackAll", async () => { + it('ackAll', async () => { await Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -924,7 +924,7 @@ describe("amqplib instrumentation promise model", () => { expectConsumeEndSpyStatus([EndOperation.AckAll, EndOperation.AckAll]); }); - it("nackAll", async () => { + it('nackAll', async () => { await Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -937,16 +937,16 @@ describe("amqplib instrumentation promise model", () => { ]); // assert all 2 span messages are ended by calling nackAll expect(getTestSpans().length).toBe(4); - lodash.range(2, 4).forEach((i) => { + lodash.range(2, 4).forEach(i => { expect(getTestSpans()[i].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[i].status.message).toEqual( - "nackAll called on message without requeue" + 'nackAll called on message without requeue' ); }); expectConsumeEndSpyStatus([EndOperation.NackAll, EndOperation.NackAll]); }); - it("reject", async () => { + it('reject', async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -954,17 +954,17 @@ describe("amqplib instrumentation promise model", () => { ); await asyncConsume(confirmChannel, queueName, [ - (msg) => confirmChannel.reject(msg, false), + msg => confirmChannel.reject(msg, false), ]); expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - "reject called on message without requeue" + 'reject called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Reject]); }); - it("reject with requeue", async () => { + it('reject with requeue', async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) @@ -972,31 +972,31 @@ describe("amqplib instrumentation promise model", () => { ); await asyncConsume(confirmChannel, queueName, [ - (msg) => confirmChannel.reject(msg, true), - (msg) => confirmChannel.reject(msg, false), + msg => confirmChannel.reject(msg, true), + msg => confirmChannel.reject(msg, false), ]); expect(getTestSpans().length).toBe(3); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[1].status.message).toEqual( - "reject called on message with requeue" + 'reject called on message with requeue' ); expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.ERROR); expect(getTestSpans()[2].status.message).toEqual( - "reject called on message without requeue" + 'reject called on message without requeue' ); expectConsumeEndSpyStatus([EndOperation.Reject, EndOperation.Reject]); }); - it("closing channel should end all open spans on it", async () => { + it('closing channel should end all open spans on it', async () => { await Promise.all( lodash.times(1, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) ) ); - await new Promise((resolve) => + await new Promise(resolve => asyncConsume(confirmChannel, queueName, [ - async (msg) => { + async msg => { await confirmChannel.close(); resolve(); confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; @@ -1006,23 +1006,23 @@ describe("amqplib instrumentation promise model", () => { expect(getTestSpans().length).toBe(2); expect(getTestSpans()[1].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[1].status.message).toEqual("channel closed"); + expect(getTestSpans()[1].status.message).toEqual('channel closed'); expectConsumeEndSpyStatus([EndOperation.ChannelClosed]); }); - it("error on channel should end all open spans on it", (done) => { + it('error on channel should end all open spans on it', done => { Promise.all( lodash.times(2, () => asyncConfirmSend(confirmChannel, queueName, msgPayload) ) ).then(() => { - confirmChannel.on("close", () => { + confirmChannel.on('close', () => { expect(getTestSpans().length).toBe(4); // second consume ended with valid ack, previous message not acked when channel is errored. // since we first ack the second message, it appear first in the finished spans array expect(getTestSpans()[2].status.code).toEqual(SpanStatusCode.UNSET); expect(getTestSpans()[3].status.code).toEqual(SpanStatusCode.ERROR); - expect(getTestSpans()[3].status.message).toEqual("channel error"); + expect(getTestSpans()[3].status.message).toEqual('channel error'); expectConsumeEndSpyStatus([ EndOperation.Ack, EndOperation.ChannelError, @@ -1031,7 +1031,7 @@ describe("amqplib instrumentation promise model", () => { }); asyncConsume(confirmChannel, queueName, [ null, - (msg) => { + msg => { try { confirmChannel.ack(msg); confirmChannel[CHANNEL_CLOSED_IN_TEST] = true; @@ -1043,7 +1043,7 @@ describe("amqplib instrumentation promise model", () => { }); }); - it("not acking the message trigger timeout", async () => { + it('not acking the message trigger timeout', async () => { instrumentation.setConfig({ consumeEndHook: endHookSpy, consumeTimeoutMs: 1, @@ -1058,25 +1058,25 @@ describe("amqplib instrumentation promise model", () => { await asyncConsume(confirmChannel, queueName, [null]); // we have timeout of 1 ms, so we wait more than that and check span indeed ended - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 10)); expect(getTestSpans().length).toBe(2); expectConsumeEndSpyStatus([EndOperation.InstrumentationTimeout]); }); }); - describe("routing and exchange", () => { - it("topic exchange", async () => { - const exchangeName = "topic exchange"; - const routingKey = "topic.name.from.unittest"; - await confirmChannel.assertExchange(exchangeName, "topic", { + describe('routing and exchange', () => { + it('topic exchange', async () => { + const exchangeName = 'topic exchange'; + const routingKey = 'topic.name.from.unittest'; + await confirmChannel.assertExchange(exchangeName, 'topic', { durable: false, }); - const { queue: queueName } = await confirmChannel.assertQueue("", { + const { queue: queueName } = await confirmChannel.assertQueue('', { durable: false, }); - await confirmChannel.bindQueue(queueName, exchangeName, "#"); + await confirmChannel.bindQueue(queueName, exchangeName, '#'); await asyncConfirmPublish( confirmChannel, @@ -1095,7 +1095,7 @@ describe("amqplib instrumentation promise model", () => { expect(publishSpan.kind).toEqual(SpanKind.PRODUCER); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -1109,16 +1109,16 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(routingKey); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( publishSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); // assert consume span expect(consumeSpan.kind).toEqual(SpanKind.CONSUMER); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_SYSTEM] - ).toEqual("rabbitmq"); + ).toEqual('rabbitmq'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_DESTINATION] ).toEqual(exchangeName); @@ -1132,10 +1132,10 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(routingKey); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL] - ).toEqual("AMQP"); + ).toEqual('AMQP'); expect( consumeSpan.attributes[SemanticAttributes.MESSAGING_PROTOCOL_VERSION] - ).toEqual("0.9.1"); + ).toEqual('0.9.1'); // assert context propagation expect(consumeSpan.spanContext().traceId).toEqual( @@ -1147,28 +1147,28 @@ describe("amqplib instrumentation promise model", () => { }); }); - describe("hooks", () => { - it("publish and consume hooks success", async () => { - const attributeNameFromHook = "attribute.name.from.hook"; - const hookAttributeValue = "attribute value from hook"; + describe('hooks', () => { + it('publish and consume hooks success', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; const attributeNameFromConfirmEndHook = - "attribute.name.from.confirm.endhook"; + 'attribute.name.from.confirm.endhook'; const confirmEndHookAttributeValue = - "attribute value from confirm end hook"; + 'attribute value from confirm end hook'; const attributeNameFromConsumeEndHook = - "attribute.name.from.consume.endhook"; + 'attribute.name.from.consume.endhook'; const consumeEndHookAttributeValue = - "attribute value from consume end hook"; + 'attribute value from consume end hook'; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo) => { - expect(publishParams.exchange).toEqual(""); + expect(publishParams.exchange).toEqual(''); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); expect(publishParams.isConfirmChannel).toBe(true); span.setAttribute(attributeNameFromHook, hookAttributeValue); }, publishConfirmHook: (span, publishParams) => { - expect(publishParams.exchange).toEqual(""); + expect(publishParams.exchange).toEqual(''); expect(publishParams.routingKey).toEqual(queueName); expect(publishParams.content.toString()).toEqual(msgPayload); span.setAttribute( @@ -1212,24 +1212,24 @@ describe("amqplib instrumentation promise model", () => { ).toEqual(consumeEndHookAttributeValue); }); - it("hooks throw should not affect user flow or span creation", async () => { - const attributeNameFromHook = "attribute.name.from.hook"; - const hookAttributeValue = "attribute value from hook"; + it('hooks throw should not affect user flow or span creation', async () => { + const attributeNameFromHook = 'attribute.name.from.hook'; + const hookAttributeValue = 'attribute value from hook'; instrumentation.setConfig({ publishHook: (span: Span, publishParams: PublishInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error("error from hook"); + throw new Error('error from hook'); }, publishConfirmHook: ( span: Span, publishParams: PublishInfo ): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error("error from hook"); + throw new Error('error from hook'); }, consumeHook: (span: Span, consumeInfo: ConsumeInfo): void => { span.setAttribute(attributeNameFromHook, hookAttributeValue); - throw new Error("error from hook"); + throw new Error('error from hook'); }, }); @@ -1239,7 +1239,7 @@ describe("amqplib instrumentation promise model", () => { noAck: true, }); expect(getTestSpans().length).toBe(2); - getTestSpans().forEach((s) => + getTestSpans().forEach(s => expect(s.attributes[attributeNameFromHook]).toEqual( hookAttributeValue ) From 68697924fae4c25776050361018d1ca4d0fc2ad9 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 15:14:51 +0200 Subject: [PATCH 30/39] refactor(amqplib): cleanup setting config in instrumentations --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 5abc1c4a8e..5d0e4ab4b9 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -61,7 +61,7 @@ import { VERSION } from './version'; export class AmqplibInstrumentation extends InstrumentationBase { protected override _config!: AmqplibInstrumentationConfig; - constructor(config: AmqplibInstrumentationConfig = {}) { + constructor(config?: AmqplibInstrumentationConfig) { super( '@opentelemetry/instrumentation-amqplib', VERSION, From 3b44cdacec80c2f4c2573f9ae323debbfc590371 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 15:19:06 +0200 Subject: [PATCH 31/39] chore(amqplib): move amqplib from release please manifest to config --- .release-please-manifest.json | 1 - release-please-config.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 431fd06d84..4eb8a15a59 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -9,7 +9,6 @@ "packages/opentelemetry-host-metrics": "0.27.1", "packages/opentelemetry-id-generator-aws-xray": "1.0.1", "packages/opentelemetry-test-utils": "0.29.0", - "plugins/node/instrumentation-amwplib": "0.27.0", "plugins/node/instrumentation-tedious": "0.1.0", "plugins/node/opentelemetry-instrumentation-aws-lambda": "0.29.0", "plugins/node/opentelemetry-instrumentation-aws-sdk": "0.5.0", diff --git a/release-please-config.json b/release-please-config.json index 85dac87950..f5221202b7 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -17,6 +17,7 @@ "packages/opentelemetry-propagation-utils": {}, "packages/opentelemetry-test-utils": {}, "plugins/node/instrumentation-tedious": {}, + "plugins/node/opentelemetry-instrumentation-amqplib": {}, "plugins/node/opentelemetry-instrumentation-aws-lambda": {}, "plugins/node/opentelemetry-instrumentation-aws-sdk": {}, "plugins/node/opentelemetry-instrumentation-bunyan": {}, From 4f474edb8830b4f948949ae9f520808723c54852 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 15:23:33 +0200 Subject: [PATCH 32/39] revert(amqplib): revert release please manifest --- .release-please-manifest.json | 46 +---------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4eb8a15a59..47b1adabb2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,45 +1 @@ -{ - "detectors/node/opentelemetry-resource-detector-alibaba-cloud": "0.26.2", - "detectors/node/opentelemetry-resource-detector-aws": "1.0.3", - "detectors/node/opentelemetry-resource-detector-gcp": "0.26.2", - "detectors/node/opentelemetry-resource-detector-github": "0.26.1", - "metapackages/auto-instrumentations-node": "0.27.3", - "metapackages/auto-instrumentations-web": "0.27.2", - "packages/opentelemetry-browser-extension-autoinjection": "0.27.3", - "packages/opentelemetry-host-metrics": "0.27.1", - "packages/opentelemetry-id-generator-aws-xray": "1.0.1", - "packages/opentelemetry-test-utils": "0.29.0", - "plugins/node/instrumentation-tedious": "0.1.0", - "plugins/node/opentelemetry-instrumentation-aws-lambda": "0.29.0", - "plugins/node/opentelemetry-instrumentation-aws-sdk": "0.5.0", - "plugins/node/opentelemetry-instrumentation-bunyan": "0.27.1", - "plugins/node/opentelemetry-instrumentation-cassandra": "0.27.1", - "plugins/node/opentelemetry-instrumentation-connect": "0.27.1", - "plugins/node/opentelemetry-instrumentation-dns": "0.27.1", - "plugins/node/opentelemetry-instrumentation-express": "0.28.0", - "plugins/node/opentelemetry-instrumentation-generic-pool": "0.27.2", - "plugins/node/opentelemetry-instrumentation-graphql": "0.27.3", - "plugins/node/opentelemetry-instrumentation-hapi": "0.27.1", - "plugins/node/opentelemetry-instrumentation-ioredis": "0.27.1", - "plugins/node/opentelemetry-instrumentation-knex": "0.27.1", - "plugins/node/opentelemetry-instrumentation-koa": "0.28.1", - "plugins/node/opentelemetry-instrumentation-memcached": "0.27.1", - "plugins/node/opentelemetry-instrumentation-mongodb": "0.28.0", - "plugins/node/opentelemetry-instrumentation-mysql": "0.27.1", - "plugins/node/opentelemetry-instrumentation-mysql2": "0.28.0", - "plugins/node/opentelemetry-instrumentation-nestjs-core": "0.28.3", - "plugins/node/opentelemetry-instrumentation-net": "0.27.1", - "plugins/node/opentelemetry-instrumentation-pg": "0.28.0", - "plugins/node/opentelemetry-instrumentation-pino": "0.28.1", - "plugins/node/opentelemetry-instrumentation-redis": "0.28.0", - "plugins/node/opentelemetry-instrumentation-restify": "0.27.2", - "plugins/node/opentelemetry-instrumentation-router": "0.27.1", - "plugins/node/opentelemetry-instrumentation-winston": "0.27.1", - "plugins/web/opentelemetry-instrumentation-document-load": "0.27.1", - "plugins/web/opentelemetry-instrumentation-user-interaction": "0.28.1", - "plugins/web/opentelemetry-plugin-react-load": "0.26.1", - "propagators/opentelemetry-propagator-aws-xray": "1.0.1", - "propagators/opentelemetry-propagator-grpc-census-binary": "0.25.1", - "propagators/opentelemetry-propagator-ot-trace": "0.25.1", - "plugins/node/opentelemetry-instrumentation-fastify": "0.25.0" -} \ No newline at end of file +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.26.2","detectors/node/opentelemetry-resource-detector-aws":"1.0.3","detectors/node/opentelemetry-resource-detector-gcp":"0.26.2","detectors/node/opentelemetry-resource-detector-github":"0.26.1","metapackages/auto-instrumentations-node":"0.27.3","metapackages/auto-instrumentations-web":"0.27.2","packages/opentelemetry-browser-extension-autoinjection":"0.27.3","packages/opentelemetry-host-metrics":"0.27.1","packages/opentelemetry-id-generator-aws-xray":"1.0.1","packages/opentelemetry-test-utils":"0.29.0","plugins/node/instrumentation-tedious":"0.1.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.29.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.5.0","plugins/node/opentelemetry-instrumentation-bunyan":"0.27.1","plugins/node/opentelemetry-instrumentation-cassandra":"0.27.1","plugins/node/opentelemetry-instrumentation-connect":"0.27.1","plugins/node/opentelemetry-instrumentation-dns":"0.27.1","plugins/node/opentelemetry-instrumentation-express":"0.28.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.27.2","plugins/node/opentelemetry-instrumentation-graphql":"0.27.3","plugins/node/opentelemetry-instrumentation-hapi":"0.27.1","plugins/node/opentelemetry-instrumentation-ioredis":"0.27.1","plugins/node/opentelemetry-instrumentation-knex":"0.27.1","plugins/node/opentelemetry-instrumentation-koa":"0.28.1","plugins/node/opentelemetry-instrumentation-memcached":"0.27.1","plugins/node/opentelemetry-instrumentation-mongodb":"0.28.0","plugins/node/opentelemetry-instrumentation-mysql":"0.27.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.28.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.28.3","plugins/node/opentelemetry-instrumentation-net":"0.27.1","plugins/node/opentelemetry-instrumentation-pg":"0.28.0","plugins/node/opentelemetry-instrumentation-pino":"0.28.1","plugins/node/opentelemetry-instrumentation-redis":"0.28.0","plugins/node/opentelemetry-instrumentation-restify":"0.27.2","plugins/node/opentelemetry-instrumentation-router":"0.27.1","plugins/node/opentelemetry-instrumentation-winston":"0.27.1","plugins/web/opentelemetry-instrumentation-document-load":"0.27.1","plugins/web/opentelemetry-instrumentation-user-interaction":"0.28.1","plugins/web/opentelemetry-plugin-react-load":"0.26.1","propagators/opentelemetry-propagator-aws-xray":"1.0.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.25.1","propagators/opentelemetry-propagator-ot-trace":"0.25.1","plugins/node/opentelemetry-instrumentation-fastify":"0.25.0"} \ No newline at end of file From cf501664b98b12c729440a0177a9d51a2435619b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 15:26:14 +0200 Subject: [PATCH 33/39] chore(amqplib): restore removed copyright word in LICENSE --- plugins/node/instrumentation-amqplib/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/LICENSE b/plugins/node/instrumentation-amqplib/LICENSE index a62e729e4a..e50e8c80f9 100644 --- a/plugins/node/instrumentation-amqplib/LICENSE +++ b/plugins/node/instrumentation-amqplib/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - [2022] OpenTelemetry Authors + Copyright [2022] OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From d3ef9639234c7bd36c55bf62b341278103a8bb54 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Feb 2022 17:42:31 +0200 Subject: [PATCH 34/39] fix(amqplib): use root context to extract propagation headers --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 5d0e4ab4b9..92354c6944 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -21,6 +21,7 @@ import { Span, SpanKind, SpanStatusCode, + ROOT_CONTEXT, } from '@opentelemetry/api'; import { InstrumentationBase, @@ -398,7 +399,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { } const headers = msg.properties.headers ?? {}; - const parentContext = propagation.extract(context.active(), headers); + const parentContext = propagation.extract(ROOT_CONTEXT, headers); const exchange = msg.fields?.exchange; const span = self.tracer.startSpan( `${queue} process`, From 59f32cc5ff614f0a6ce523d67afd1dde3287bb98 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 21 Feb 2022 18:37:50 +0200 Subject: [PATCH 35/39] chore(amqplib): remove codecov --- plugins/node/instrumentation-amqplib/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/package.json b/plugins/node/instrumentation-amqplib/package.json index c8d9320cec..52b436f5b9 100644 --- a/plugins/node/instrumentation-amqplib/package.json +++ b/plugins/node/instrumentation-amqplib/package.json @@ -29,7 +29,6 @@ "repository": "open-telemetry/opentelemetry-js-contrib", "scripts": { "clean": "rimraf build/*", - "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "compile": "npm run version:update && tsc -p .", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", @@ -65,7 +64,6 @@ "ts-mocha": "8.0.0", "nyc": "15.1.0", "gts": "3.1.0", - "codecov": "3.8.3", "@opentelemetry/contrib-test-utils": "^0.28.0", "sinon": "13.0.1", "test-all-versions": "5.0.1", From 17270b9f6cf21defdc9a6d0553baa657de033590 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 21 Feb 2022 19:07:35 +0200 Subject: [PATCH 36/39] feat(amqplib): use performance time instead of Date --- plugins/node/instrumentation-amqplib/src/amqplib.ts | 11 ++++++----- plugins/node/instrumentation-amqplib/src/utils.ts | 3 ++- .../test/amqplib-callbacks.test.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 92354c6944..42d9e4255c 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -23,6 +23,7 @@ import { SpanStatusCode, ROOT_CONTEXT, } from '@opentelemetry/api'; +import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; import { InstrumentationBase, InstrumentationModuleDefinition, @@ -439,7 +440,7 @@ export class AmqplibInstrumentation extends InstrumentationBase { // store the message on the channel so we can close the span on ackAll etc channel[CHANNEL_SPANS_NOT_ENDED]!.push({ msg, - timeOfConsume: new Date(), + timeOfConsume: hrTime(), }); // store the span on the message, so we can end it when user call 'ack' on it @@ -712,14 +713,14 @@ export class AmqplibInstrumentation extends InstrumentationBase { } private checkConsumeTimeoutOnChannel(channel: InstrumentationConsumeChannel) { - const currentTime = new Date().getTime(); - const spansNotEnded: { msg: amqp.Message; timeOfConsume: Date }[] = + const currentTime = hrTime(); + const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; let i: number; for (i = 0; i < spansNotEnded.length; i++) { const currMessage = spansNotEnded[i]; - const timeFromConsume = currentTime - currMessage.timeOfConsume.getTime(); - if (timeFromConsume < this._config.consumeTimeoutMs!) { + const timeFromConsume = hrTimeDuration(currMessage.timeOfConsume, currentTime); + if (hrTimeToMilliseconds(timeFromConsume) < this._config.consumeTimeoutMs!) { break; } this.endConsumerSpan( diff --git a/plugins/node/instrumentation-amqplib/src/utils.ts b/plugins/node/instrumentation-amqplib/src/utils.ts index 4f7f479c96..d690e75bd7 100644 --- a/plugins/node/instrumentation-amqplib/src/utils.ts +++ b/plugins/node/instrumentation-amqplib/src/utils.ts @@ -17,6 +17,7 @@ import { Context, createContextKey, diag, + HrTime, Span, SpanAttributes, SpanAttributeValue, @@ -45,7 +46,7 @@ export type InstrumentationConsumeChannel = amqp.Channel & { connection: { [CONNECTION_ATTRIBUTES]: SpanAttributes }; [CHANNEL_SPANS_NOT_ENDED]?: { msg: amqp.ConsumeMessage; - timeOfConsume: Date; + timeOfConsume: HrTime; }[]; [CHANNEL_CONSUME_TIMEOUT_TIMER]?: NodeJS.Timer; }; diff --git a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts index e4ab04ad7e..65973d5b1f 100644 --- a/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts +++ b/plugins/node/instrumentation-amqplib/test/amqplib-callbacks.test.ts @@ -45,7 +45,7 @@ describe('amqplib instrumentation callback model', () => { before(done => { amqpCallback.connect(rabbitMqUrl, (err, connection) => { conn = connection; - done(); + done(err); }); }); after(done => { From 5481878b7dc83d7ff32dc4937aba5bccec095958 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 21 Feb 2022 19:11:56 +0200 Subject: [PATCH 37/39] chore(amqplib): lint fix --- .../instrumentation-amqplib/src/amqplib.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/src/amqplib.ts b/plugins/node/instrumentation-amqplib/src/amqplib.ts index 42d9e4255c..5d51a3dde5 100644 --- a/plugins/node/instrumentation-amqplib/src/amqplib.ts +++ b/plugins/node/instrumentation-amqplib/src/amqplib.ts @@ -23,7 +23,11 @@ import { SpanStatusCode, ROOT_CONTEXT, } from '@opentelemetry/api'; -import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; +import { + hrTime, + hrTimeDuration, + hrTimeToMilliseconds, +} from '@opentelemetry/core'; import { InstrumentationBase, InstrumentationModuleDefinition, @@ -714,13 +718,17 @@ export class AmqplibInstrumentation extends InstrumentationBase { private checkConsumeTimeoutOnChannel(channel: InstrumentationConsumeChannel) { const currentTime = hrTime(); - const spansNotEnded = - channel[CHANNEL_SPANS_NOT_ENDED] ?? []; + const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; let i: number; for (i = 0; i < spansNotEnded.length; i++) { const currMessage = spansNotEnded[i]; - const timeFromConsume = hrTimeDuration(currMessage.timeOfConsume, currentTime); - if (hrTimeToMilliseconds(timeFromConsume) < this._config.consumeTimeoutMs!) { + const timeFromConsume = hrTimeDuration( + currMessage.timeOfConsume, + currentTime + ); + if ( + hrTimeToMilliseconds(timeFromConsume) < this._config.consumeTimeoutMs! + ) { break; } this.endConsumerSpan( From 9daf145eb17c9a87cf6e4f6a6b08f433f6fea09f Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 28 Feb 2022 22:56:05 +0200 Subject: [PATCH 38/39] chore: add amqplib to release please manifest --- .release-please-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8689c5a830..0bea91a975 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.26.2","detectors/node/opentelemetry-resource-detector-aws":"1.0.3","detectors/node/opentelemetry-resource-detector-gcp":"0.26.2","detectors/node/opentelemetry-resource-detector-github":"0.26.1","metapackages/auto-instrumentations-node":"0.27.3","metapackages/auto-instrumentations-web":"0.27.2","packages/opentelemetry-browser-extension-autoinjection":"0.27.3","packages/opentelemetry-host-metrics":"0.27.1","packages/opentelemetry-id-generator-aws-xray":"1.0.1","packages/opentelemetry-test-utils":"0.29.0","plugins/node/instrumentation-tedious":"0.1.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.29.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.5.0","plugins/node/opentelemetry-instrumentation-bunyan":"0.27.1","plugins/node/opentelemetry-instrumentation-cassandra":"0.27.1","plugins/node/opentelemetry-instrumentation-connect":"0.27.1","plugins/node/opentelemetry-instrumentation-dns":"0.27.1","plugins/node/opentelemetry-instrumentation-express":"0.28.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.27.2","plugins/node/opentelemetry-instrumentation-graphql":"0.27.3","plugins/node/opentelemetry-instrumentation-hapi":"0.27.1","plugins/node/opentelemetry-instrumentation-ioredis":"0.27.1","plugins/node/opentelemetry-instrumentation-knex":"0.27.1","plugins/node/opentelemetry-instrumentation-koa":"0.28.1","plugins/node/opentelemetry-instrumentation-memcached":"0.27.1","plugins/node/opentelemetry-instrumentation-mongodb":"0.28.0","plugins/node/opentelemetry-instrumentation-mysql":"0.27.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.28.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.28.3","plugins/node/opentelemetry-instrumentation-net":"0.27.1","plugins/node/opentelemetry-instrumentation-pg":"0.28.0","plugins/node/opentelemetry-instrumentation-pino":"0.28.1","plugins/node/opentelemetry-instrumentation-redis":"0.28.0","plugins/node/opentelemetry-instrumentation-restify":"0.27.2","plugins/node/opentelemetry-instrumentation-router":"0.27.1","plugins/node/opentelemetry-instrumentation-winston":"0.27.1","plugins/web/opentelemetry-instrumentation-document-load":"0.27.1","plugins/web/opentelemetry-instrumentation-user-interaction":"0.28.1","plugins/web/opentelemetry-plugin-react-load":"0.26.1","propagators/opentelemetry-propagator-aws-xray":"1.0.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.25.1","propagators/opentelemetry-propagator-ot-trace":"0.25.1","plugins/node/opentelemetry-instrumentation-fastify":"0.25.0","packages/opentelemetry-propagation-utils":"0.26.0","plugins/web/opentelemetry-instrumentation-long-task":"0.27.0"} \ No newline at end of file +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.26.2","detectors/node/opentelemetry-resource-detector-aws":"1.0.3","detectors/node/opentelemetry-resource-detector-gcp":"0.26.2","detectors/node/opentelemetry-resource-detector-github":"0.26.1","metapackages/auto-instrumentations-node":"0.27.3","metapackages/auto-instrumentations-web":"0.27.2","packages/opentelemetry-browser-extension-autoinjection":"0.27.3","packages/opentelemetry-host-metrics":"0.27.1","packages/opentelemetry-id-generator-aws-xray":"1.0.1","packages/opentelemetry-test-utils":"0.29.0","plugins/node/instrumentation-amqplib":"0.27.0","plugins/node/instrumentation-tedious":"0.1.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.29.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.5.0","plugins/node/opentelemetry-instrumentation-bunyan":"0.27.1","plugins/node/opentelemetry-instrumentation-cassandra":"0.27.1","plugins/node/opentelemetry-instrumentation-connect":"0.27.1","plugins/node/opentelemetry-instrumentation-dns":"0.27.1","plugins/node/opentelemetry-instrumentation-express":"0.28.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.27.2","plugins/node/opentelemetry-instrumentation-graphql":"0.27.3","plugins/node/opentelemetry-instrumentation-hapi":"0.27.1","plugins/node/opentelemetry-instrumentation-ioredis":"0.27.1","plugins/node/opentelemetry-instrumentation-knex":"0.27.1","plugins/node/opentelemetry-instrumentation-koa":"0.28.1","plugins/node/opentelemetry-instrumentation-memcached":"0.27.1","plugins/node/opentelemetry-instrumentation-mongodb":"0.28.0","plugins/node/opentelemetry-instrumentation-mysql":"0.27.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.28.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.28.3","plugins/node/opentelemetry-instrumentation-net":"0.27.1","plugins/node/opentelemetry-instrumentation-pg":"0.28.0","plugins/node/opentelemetry-instrumentation-pino":"0.28.1","plugins/node/opentelemetry-instrumentation-redis":"0.28.0","plugins/node/opentelemetry-instrumentation-restify":"0.27.2","plugins/node/opentelemetry-instrumentation-router":"0.27.1","plugins/node/opentelemetry-instrumentation-winston":"0.27.1","plugins/web/opentelemetry-instrumentation-document-load":"0.27.1","plugins/web/opentelemetry-instrumentation-user-interaction":"0.28.1","plugins/web/opentelemetry-plugin-react-load":"0.26.1","propagators/opentelemetry-propagator-aws-xray":"1.0.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.25.1","propagators/opentelemetry-propagator-ot-trace":"0.25.1","plugins/node/opentelemetry-instrumentation-fastify":"0.25.0","packages/opentelemetry-propagation-utils":"0.26.0","plugins/web/opentelemetry-instrumentation-long-task":"0.27.0"} \ No newline at end of file From d7df46a98adff46cc881356fe15b254508af1a44 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Mon, 14 Mar 2022 11:26:40 +0200 Subject: [PATCH 39/39] fix(amqplib): rename Consumer to Consume for consistency --- plugins/node/instrumentation-amqplib/README.md | 4 ++-- plugins/node/instrumentation-amqplib/src/types.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/node/instrumentation-amqplib/README.md b/plugins/node/instrumentation-amqplib/README.md index ac201fd071..221d4ac940 100644 --- a/plugins/node/instrumentation-amqplib/README.md +++ b/plugins/node/instrumentation-amqplib/README.md @@ -54,8 +54,8 @@ amqplib instrumentation has few options available to choose from. You can set th | --------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `publishHook` | `AmqplibPublishCustomAttributeFunction` | hook for adding custom attributes before publish message is sent. | | `publishConfirmHook` | `AmqplibPublishConfirmCustomAttributeFunction` | hook for adding custom attributes after publish message is confirmed by the broker. | -| `consumeHook` | `AmqplibConsumerCustomAttributeFunction` | hook for adding custom attributes before consumer message is processed. | -| `consumeEndHook` | `AmqplibConsumerEndCustomAttributeFunction` | hook for adding custom attributes after consumer message is acked to server. | +| `consumeHook` | `AmqplibConsumeCustomAttributeFunction` | hook for adding custom attributes before consumer message is processed. | +| `consumeEndHook` | `AmqplibConsumeEndCustomAttributeFunction` | hook for adding custom attributes after consumer message is acked to server. | | `consumeTimeoutMs` | `number` | read [Consume Timeout](#ConsumeTimeout) below | ### Consume Timeout diff --git a/plugins/node/instrumentation-amqplib/src/types.ts b/plugins/node/instrumentation-amqplib/src/types.ts index 57626b524f..66c6140eeb 100644 --- a/plugins/node/instrumentation-amqplib/src/types.ts +++ b/plugins/node/instrumentation-amqplib/src/types.ts @@ -49,11 +49,11 @@ export interface AmqplibPublishConfirmCustomAttributeFunction { (span: Span, publishConfirmedInto: PublishConfirmedInfo): void; } -export interface AmqplibConsumerCustomAttributeFunction { +export interface AmqplibConsumeCustomAttributeFunction { (span: Span, consumeInfo: ConsumeInfo): void; } -export interface AmqplibConsumerEndCustomAttributeFunction { +export interface AmqplibConsumeEndCustomAttributeFunction { (span: Span, consumeEndInfo: ConsumeEndInfo): void; } @@ -77,10 +77,10 @@ export interface AmqplibInstrumentationConfig extends InstrumentationConfig { publishConfirmHook?: AmqplibPublishConfirmCustomAttributeFunction; /** hook for adding custom attributes before consumer message is processed */ - consumeHook?: AmqplibConsumerCustomAttributeFunction; + consumeHook?: AmqplibConsumeCustomAttributeFunction; /** hook for adding custom attributes after consumer message is acked to server */ - consumeEndHook?: AmqplibConsumerEndCustomAttributeFunction; + consumeEndHook?: AmqplibConsumeEndCustomAttributeFunction; /** * When user is setting up consume callback, it is user's responsibility to call