diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index bd9d228f5c9c..a6478fe50a9a 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -12966,7 +12966,7 @@ packages: dev: false file:projects/container-registry.tgz: - resolution: {integrity: sha512-DBJrNjfBDQPpuhuC1CK2Tf09gwxZR8AFbUUmjOkxzU4Ra4+k8WNYyADJUQPWDxUnCg+/wckXoRZL/jkX39lpSg==, tarball: file:projects/container-registry.tgz} + resolution: {integrity: sha512-EFTZnQnEhHbqmL1CZJWw7bgI7vYq4WRTRkjsjf8ixNAarMw/NVp3eLd+skni99Hs4F+x4ZhIihyjSIExV5yLJA==, tarball: file:projects/container-registry.tgz} name: '@rush-temp/container-registry' version: 0.0.0 dependencies: @@ -13007,7 +13007,6 @@ packages: transitivePeerDependencies: - bufferutil - debug - - encoding - supports-color - utf-8-validate dev: false @@ -13601,7 +13600,7 @@ packages: dev: false file:projects/data-tables.tgz: - resolution: {integrity: sha512-DaRzGWNUMNnkx5XtovGZ1VfqT6XHzNoh6ivGkD3kWQEGRtv11tnZ75Aguv/eG/3SromlH20PiCA+RQWcLm3FLQ==, tarball: file:projects/data-tables.tgz} + resolution: {integrity: sha512-TEbNW856SR3kFwVpkaq6xq+/lScXvhz1tDQwNF6qBlgQOSMwk5VnR+R82rTsPT0G9gSoTWrnox0ZpAja5SolDA==, tarball: file:projects/data-tables.tgz} name: '@rush-temp/data-tables' version: 0.0.0 dependencies: @@ -13646,7 +13645,6 @@ packages: - '@swc/wasm' - bufferutil - debug - - encoding - supports-color - utf-8-validate dev: false @@ -13836,11 +13834,11 @@ packages: dev: false file:projects/event-hubs.tgz: - resolution: {integrity: sha512-pBrhEn6e2Di9awJaS8boEUBHwzLdLA6udItp6pqvGjjIl4LqQZSlYinDrlevKddv+aijwT0GXH1VcJJ8ANBxZQ==, tarball: file:projects/event-hubs.tgz} + resolution: {integrity: sha512-itrh9O/PoTinG39ND5MqHEWiUIvLDTNSZvBlCO4LmU7MliEmXRHUhN4p99BWuXPf89pfZ5e08o7Ad+rmdpzXGQ==, tarball: file:projects/event-hubs.tgz} name: '@rush-temp/event-hubs' version: 0.0.0 dependencies: - '@azure/core-tracing': 1.0.0-preview.13 + '@azure/core-tracing': 1.0.0-preview.14 '@microsoft/api-extractor': 7.19.4 '@rollup/plugin-commonjs': 21.0.1_rollup@2.67.1 '@rollup/plugin-inject': 4.0.4_rollup@2.67.1 @@ -13965,7 +13963,7 @@ packages: dev: false file:projects/eventhubs-checkpointstore-blob.tgz: - resolution: {integrity: sha512-Zjderk9z4yJ2X0VCX+Bg2mv4sRQQVGvPYhz4bqwQe8ekduCFek0KT0230RKdGANBSmWtYN/zbxmPbjexRtOv8A==, tarball: file:projects/eventhubs-checkpointstore-blob.tgz} + resolution: {integrity: sha512-/D9UB0TNxhGrAggxo5vIN6OmKdenUTYw+XbUlwEbi7efhTrXpxmBXPBZ9fDjYvWWurgmrcB5BmDbs79tY6R8Ow==, tarball: file:projects/eventhubs-checkpointstore-blob.tgz} name: '@rush-temp/eventhubs-checkpointstore-blob' version: 0.0.0 dependencies: @@ -15786,7 +15784,7 @@ packages: dev: false file:projects/storage-file-share.tgz: - resolution: {integrity: sha512-Wxf71HaHGk3OUsxKZVuBej88ap8Pft5N//7ZiLUVN3If1B+1fCanIE/T52XQhsyBXWsgvQFKFTehFLaryX4eIQ==, tarball: file:projects/storage-file-share.tgz} + resolution: {integrity: sha512-2ZH8fLmpPVmWUGAVGdmZ8P0MjIFOKfaaovnt9M901HdH6hzf7cA0zDJWrYWUdyBdWlJWr9/lcc7hNJgNHvRADw==, tarball: file:projects/storage-file-share.tgz} name: '@rush-temp/storage-file-share' version: 0.0.0 dependencies: diff --git a/sdk/eventhub/event-hubs/CHANGELOG.md b/sdk/eventhub/event-hubs/CHANGELOG.md index 7cfa62271161..e2a94f66c82e 100644 --- a/sdk/eventhub/event-hubs/CHANGELOG.md +++ b/sdk/eventhub/event-hubs/CHANGELOG.md @@ -10,6 +10,10 @@ ### Other Changes +- Updated our `@azure/core-tracing` dependency to the latest version (1.0.0-preview.14) + - Notable changes include Removal of `@opentelemetry/api` as a transitive dependency and ensuring that the active context is properly propagated. + - Customers who would like to continue using OpenTelemetry driven tracing should visit our [OpenTelemetry Instrumentation](https://www.npmjs.com/package/@azure/opentelemetry-instrumentation-azure-sdk) package for instructions. + ## 5.8.0-beta.1 (2022-02-08) ### Features Added diff --git a/sdk/eventhub/event-hubs/package.json b/sdk/eventhub/event-hubs/package.json index cbca7362c1cb..ad1e6114b28f 100644 --- a/sdk/eventhub/event-hubs/package.json +++ b/sdk/eventhub/event-hubs/package.json @@ -110,7 +110,7 @@ "@azure/core-amqp": "^3.1.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-tracing": "1.0.0-preview.14", "@azure/logger": "^1.0.0", "buffer": "^6.0.0", "is-buffer": "^2.0.3", diff --git a/sdk/eventhub/event-hubs/review/event-hubs.api.md b/sdk/eventhub/event-hubs/review/event-hubs.api.md index 632dd8430395..0970ec526825 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -15,8 +15,6 @@ import { OperationTracingOptions } from '@azure/core-tracing'; import { RetryMode } from '@azure/core-amqp'; import { RetryOptions } from '@azure/core-amqp'; import { SASCredential } from '@azure/core-auth'; -import { Span } from '@azure/core-tracing'; -import { SpanContext } from '@azure/core-tracing'; import { TokenCredential } from '@azure/core-auth'; import { WebSocketImpl } from 'rhea-promise'; import { WebSocketOptions } from '@azure/core-amqp'; @@ -374,8 +372,6 @@ export { TokenCredential } // @public export interface TryAddOptions { - // @deprecated (undocumented) - parentSpan?: Span | SpanContext; tracingOptions?: OperationTracingOptions; } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index 308685bcf518..a67fa0d8d3da 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -2,15 +2,10 @@ // Licensed under the MIT license. import { EventData, isAmqpAnnotatedMessage } from "../eventData"; -import { - extractSpanContextFromTraceParentHeader, - getTraceParentHeader, - isSpanContextValid, -} from "@azure/core-tracing"; +import { TracingContext } from "@azure/core-tracing"; import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { OperationOptions } from "../util/operationOptions"; -import { SpanContext } from "@azure/core-tracing"; -import { createMessageSpan } from "./tracing"; +import { toSpanOptions, tracingClient } from "./tracing"; /** * @internal @@ -29,7 +24,7 @@ export function instrumentEventData( options: OperationOptions, entityPath: string, host: string -): { event: EventData; spanContext: SpanContext | undefined } { +): { event: EventData; spanContext: TracingContext | undefined } { const props = isAmqpAnnotatedMessage(eventData) ? eventData.applicationProperties : eventData.properties; @@ -41,7 +36,11 @@ export function instrumentEventData( return { event: eventData, spanContext: undefined }; } - const { span: messageSpan } = createMessageSpan(options, { entityPath, host }); + const { span: messageSpan, updatedOptions } = tracingClient.startSpan( + "message", + options, + toSpanOptions({ entityPath, host }, "producer") + ); try { if (!messageSpan.isRecording()) { return { @@ -50,8 +49,10 @@ export function instrumentEventData( }; } - const traceParent = getTraceParentHeader(messageSpan.spanContext()); - if (traceParent && isSpanContextValid(messageSpan.spanContext())) { + const traceParent = tracingClient.createRequestHeaders( + updatedOptions.tracingOptions?.tracingContext + )["traceparent"]; + if (traceParent) { const copiedProps = { ...props }; // create a copy so the original isn't modified @@ -65,7 +66,7 @@ export function instrumentEventData( return { event: eventData, - spanContext: messageSpan.spanContext(), + spanContext: updatedOptions.tracingOptions?.tracingContext, }; } finally { messageSpan.end(); @@ -77,11 +78,11 @@ export function instrumentEventData( * @param eventData - An individual `EventData` object. * @internal */ -export function extractSpanContextFromEventData(eventData: EventData): SpanContext | undefined { +export function extractSpanContextFromEventData(eventData: EventData): TracingContext | undefined { if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { return; } const diagnosticId = eventData.properties[TRACEPARENT_PROPERTY]; - return extractSpanContextFromTraceParentHeader(diagnosticId); + return tracingClient.parseTraceparentHeader(diagnosticId); } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts b/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts index 0e87bf4140c0..ce9ab5b256ea 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts @@ -1,136 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - Span, - SpanContext, - SpanKind, - SpanOptions, - context, - createSpanFunction, - setSpan, - setSpanContext, -} from "@azure/core-tracing"; +import { createTracingClient, TracingSpanOptions, TracingSpanKind } from "@azure/core-tracing"; import { EventHubConnectionConfig } from "../eventhubConnectionConfig"; -import { OperationOptions } from "../util/operationOptions"; -import { TryAddOptions } from "../eventDataBatch"; +import { packageJsonInfo } from "../util/constants"; -const _createSpan = createSpanFunction({ +/** + * The {@link TracingClient} that is used to add tracing spans. + */ +export const tracingClient = createTracingClient({ namespace: "Microsoft.EventHub", - packagePrefix: "Azure.EventHubs", + packageName: packageJsonInfo.name, + packageVersion: packageJsonInfo.version, }); /** - * Creates an EventHubs specific span, with peer.address and message_bus.destination filled out. - * @internal + * Creates {@link TracingSpanOptions} from the provided data. + * @param eventHubConfig - The configuration object containing initial attributes to set on the span. + * @param spanKind - The {@link TracingSpanKind} for the newly created span. + * @returns a {@link TracingSpanOptions} that can be passed to a {@link TracingClient} */ -export function createEventHubSpan( - operationName: string, - operationOptions: OperationOptions | undefined, - connectionConfig: Pick, - additionalSpanOptions?: SpanOptions -): { span: Span; updatedOptions: OperationOptions } { - const { span, updatedOptions } = _createSpan(operationName, { - ...operationOptions, - tracingOptions: { - ...operationOptions?.tracingOptions, - spanOptions: { - // By passing spanOptions if they exist at runtime, we're backwards compatible with @azure/core-tracing@preview.13 and earlier. - ...(operationOptions?.tracingOptions as any)?.spanOptions, - ...additionalSpanOptions, - }, +export function toSpanOptions( + eventHubConfig: Pick, + spanKind?: TracingSpanKind +): TracingSpanOptions { + const spanOptions: TracingSpanOptions = { + spanAttributes: { + "message_bus.destination": eventHubConfig.entityPath, + "peer.address": eventHubConfig.host, }, - }); - - span.setAttribute("message_bus.destination", connectionConfig.entityPath); - span.setAttribute("peer.address", connectionConfig.host); - - return { - span, - updatedOptions, }; -} - -/** - * @internal - */ -export function createMessageSpan( - operationOptions: OperationOptions, - eventHubConfig: Pick -): ReturnType { - return createEventHubSpan("message", operationOptions, eventHubConfig, { - kind: SpanKind.PRODUCER, - }); -} - -/** - * Converts TryAddOptions into the modern shape (OperationOptions) when needed. - * (this is something we can eliminate at the next major release of EH _or_ when - * we release with the GA version of opentelemetry). - * - * @internal - */ -export function convertTryAddOptionsForCompatibility(tryAddOptions: TryAddOptions): TryAddOptions { - /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ - // @ts-ignore: parentSpan is deprecated and this is compat code to translate it until we can get rid of it. - const legacyParentSpanOrSpanContext = tryAddOptions.parentSpan; - - /* - Our goal here is to offer compatibility but there is a case where a user might accidentally pass - _both_ sets of options. We'll assume they want the OperationTracingOptions code path in that case. - - Example of accidental span passing: - - const someOptionsPassedIntoTheirFunction = { - parentSpan: span; // set somewhere else in their code - } - - function takeSomeOptionsFromSomewhere(someOptionsPassedIntoTheirFunction) { - - batch.tryAddMessage(message, { - // "runtime" blend of options from some other part of their app - ...someOptionsPassedIntoTheirFunction, // parentSpan comes along for the ride... - - tracingOptions: { - // thank goodness, I'm doing this right! (thinks the developer) - spanOptions: { - context: context - } - } - }); - } - - And now they've accidentally been opted into the legacy code path even though they think - they're using the modern code path. - - This does kick the can down the road a bit - at some point we will be putting them in this - situation where things looked okay but their spans are becoming unparented but we can - try to announce this (and other changes related to tracing) in our next big rev. - */ - - if (!legacyParentSpanOrSpanContext || tryAddOptions.tracingOptions) { - // assume that the options are already in the modern shape even if (possibly) - // they were still specifying `parentSpan` - return tryAddOptions; - } - - const convertedOptions: TryAddOptions = { - ...tryAddOptions, - tracingOptions: { - tracingContext: isSpan(legacyParentSpanOrSpanContext) - ? setSpan(context.active(), legacyParentSpanOrSpanContext) - : setSpanContext(context.active(), legacyParentSpanOrSpanContext), - }, - }; - - return convertedOptions; -} - -function isSpan(possibleSpan: Span | SpanContext | undefined): possibleSpan is Span { - if (possibleSpan == null) { - return false; + if (spanKind) { + spanOptions.spanKind = spanKind; } - - const x = possibleSpan as Span; - return typeof x.spanContext === "function"; + return spanOptions; } diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index d7d4dfcd44b5..6ef96c87a489 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -5,10 +5,8 @@ import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { EventData, populateIdempotentMessageAnnotations, toRheaMessage } from "./eventData"; import { ConnectionContext } from "./connectionContext"; import { MessageAnnotations, message, Message as RheaMessage } from "rhea-promise"; -import { Span, SpanContext } from "@azure/core-tracing"; import { isDefined, isObjectWithProperties } from "./util/typeGuards"; -import { OperationTracingOptions } from "@azure/core-tracing"; -import { convertTryAddOptionsForCompatibility } from "./diagnostics/tracing"; +import { OperationTracingOptions, TracingContext } from "@azure/core-tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; import { throwTypeErrorIfParameterMissing } from "./util/error"; import { PartitionPublishingProperties } from "./models/private"; @@ -48,11 +46,6 @@ export interface TryAddOptions { * The options to use when creating Spans for tracing. */ tracingOptions?: OperationTracingOptions; - - /** - * @deprecated Tracing options have been moved to the `tracingOptions` property. - */ - parentSpan?: Span | SpanContext; } /** @@ -153,7 +146,7 @@ export class EventDataBatchImpl implements EventDataBatch { /** * List of 'message' span contexts. */ - private _spanContexts: SpanContext[] = []; + private _spanContexts: TracingContext[] = []; /** * The message annotations to apply on the batch envelope. * This will reflect the message annotations on the first event @@ -256,7 +249,7 @@ export class EventDataBatchImpl implements EventDataBatch { * Gets the "message" span contexts that were created when adding events to the batch. * @internal */ - get _messageSpanContexts(): SpanContext[] { + get _messageSpanContexts(): TracingContext[] { return this._spanContexts; } @@ -377,7 +370,6 @@ export class EventDataBatchImpl implements EventDataBatch { */ public tryAdd(eventData: EventData | AmqpAnnotatedMessage, options: TryAddOptions = {}): boolean { throwTypeErrorIfParameterMissing(this._context.connectionId, "tryAdd", "eventData", eventData); - options = convertTryAddOptionsForCompatibility(options); const { entityPath, host } = this._context.config; const { event: instrumentedEvent, spanContext } = instrumentEventData( diff --git a/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts b/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts index d0ffa2be5aae..99e0371d9db5 100644 --- a/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts @@ -13,7 +13,7 @@ import { import { PartitionPublishingOptions, PartitionPublishingProperties } from "./models/private"; import { EventDataBatch, EventDataBatchImpl, isEventDataBatch } from "./eventDataBatch"; import { EventHubProperties, PartitionProperties } from "./managementClient"; -import { Link, Span, SpanContext, SpanKind, SpanStatusCode } from "@azure/core-tracing"; +import { TracingContext, TracingSpanLink } from "@azure/core-tracing"; import { NamedKeyCredential, SASCredential, TokenCredential } from "@azure/core-auth"; import { isCredential, isDefined } from "./util/typeGuards"; import { logErrorStackTrace, logger } from "./log"; @@ -28,7 +28,7 @@ import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { EventData, EventDataInternal } from "./eventData"; import { EventHubSender } from "./eventHubSender"; import { OperationOptions } from "./util/operationOptions"; -import { createEventHubSpan } from "./diagnostics/tracing"; +import { toSpanOptions, tracingClient } from "./diagnostics/tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; /** @@ -378,14 +378,15 @@ export class EventHubProducerClient { let partitionId: string | undefined; let partitionKey: string | undefined; - // link message span contexts - let spanContextsToLink: SpanContext[] = []; // Holds an EventData properties object containing tracing properties. // This lets us avoid cloning batch when it is EventData[], which is // important as the idempotent EventHubSender needs to decorate the // original EventData passed through. const eventDataTracingProperties: Array = []; + // link message span contexts + let spanContextsToLink: TracingContext[] = []; + if (isEventDataBatch(batch)) { if ( this._enableIdempotentPartitions && @@ -425,40 +426,37 @@ export class EventHubProducerClient { partitionKey, }); - let sender = this._sendersMap.get(partitionId || ""); - if (!sender) { - const partitionPublishingOptions = isDefined(partitionId) - ? this._partitionOptions?.[partitionId] - : undefined; - sender = EventHubSender.create(this._context, { - enableIdempotentProducer: Boolean(this._enableIdempotentPartitions), - partitionId, - partitionPublishingOptions, - }); - this._sendersMap.set(partitionId || "", sender); - } - - const sendSpan = this._createSendSpan(options, spanContextsToLink); - - try { - const result = await sender.send(batch, { - ...options, - partitionId, - partitionKey, - retryOptions: this._clientOptions.retryOptions, - tracingProperties: eventDataTracingProperties, - }); - sendSpan.setStatus({ code: SpanStatusCode.OK }); - return result; - } catch (error) { - sendSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - throw error; - } finally { - sendSpan.end(); - } + return tracingClient.withSpan( + `${EventHubProducerClient.name}.${this.sendBatch.name}`, + options, + (updatedOptions) => { + let sender = this._sendersMap.get(partitionId || ""); + if (!sender) { + const partitionPublishingOptions = isDefined(partitionId) + ? this._partitionOptions?.[partitionId] + : undefined; + sender = EventHubSender.create(this._context, { + enableIdempotentProducer: Boolean(this._enableIdempotentPartitions), + partitionId, + partitionPublishingOptions, + }); + this._sendersMap.set(partitionId || "", sender); + } + + return sender.send(batch, { + ...updatedOptions, + partitionId, + partitionKey, + retryOptions: this._clientOptions.retryOptions, + }); + }, + { + spanLinks: spanContextsToLink.map((tracingContext) => { + return { tracingContext }; + }), + ...toSpanOptions(this._context.config, "client"), + } + ); } /** @@ -526,24 +524,6 @@ export class EventHubProducerClient { retryOptions: this._clientOptions.retryOptions, }); } - - private _createSendSpan( - operationOptions: OperationOptions, - spanContextsToLink: SpanContext[] = [] - ): Span { - const links: Link[] = spanContextsToLink.map((context) => { - return { - context, - }; - }); - - const { span } = createEventHubSpan("send", operationOptions, this._context.config, { - kind: SpanKind.CLIENT, - links, - }); - - return span; - } } /** diff --git a/sdk/eventhub/event-hubs/src/managementClient.ts b/sdk/eventhub/event-hubs/src/managementClient.ts index 3712f4f534e5..4e979c8f9d82 100644 --- a/sdk/eventhub/event-hubs/src/managementClient.ts +++ b/sdk/eventhub/event-hubs/src/managementClient.ts @@ -29,8 +29,7 @@ import { AccessToken } from "@azure/core-auth"; import { ConnectionContext } from "./connectionContext"; import { LinkEntity } from "./linkEntity"; import { OperationOptions } from "./util/operationOptions"; -import { SpanStatusCode } from "@azure/core-tracing"; -import { createEventHubSpan } from "./diagnostics/tracing"; +import { toSpanOptions, tracingClient } from "./diagnostics/tracing"; import { getRetryAttemptTimeoutInMs } from "./util/retries"; import { v4 as uuid } from "uuid"; @@ -159,52 +158,51 @@ export class ManagementClient extends LinkEntity { options: OperationOptions & { retryOptions?: RetryOptions } = {} ): Promise { throwErrorIfConnectionClosed(this._context); - const { span: clientSpan } = createEventHubSpan( - "getEventHubProperties", + return tracingClient.withSpan( + "ManagementClient.getEventHubProperties", options, - this._context.config - ); - - try { - const securityToken = await this.getSecurityToken(); - const request: Message = { - body: Buffer.from(JSON.stringify([])), - message_id: uuid(), - reply_to: this.replyTo, - application_properties: { - operation: Constants.readOperation, - name: this.entityPath as string, - type: `${Constants.vendorString}:${Constants.eventHub}`, - security_token: securityToken?.token, - }, - }; - - const info: any = await this._makeManagementRequest(request, { - ...options, - requestName: "getHubRuntimeInformation", - }); - const runtimeInfo: EventHubProperties = { - name: info.name, - createdOn: new Date(info.created_at), - partitionIds: info.partition_ids, - }; - logger.verbose("[%s] The hub runtime info is: %O", this._context.connectionId, runtimeInfo); + async (updatedOptions) => { + try { + const securityToken = await this.getSecurityToken(); + + const request: Message = { + body: Buffer.from(JSON.stringify([])), + message_id: uuid(), + reply_to: this.replyTo, + application_properties: { + operation: Constants.readOperation, + name: this.entityPath as string, + type: `${Constants.vendorString}:${Constants.eventHub}`, + security_token: securityToken?.token, + }, + }; + + const info: any = await this._makeManagementRequest(request, { + ...updatedOptions, + requestName: "getHubRuntimeInformation", + }); + const runtimeInfo: EventHubProperties = { + name: info.name, + createdOn: new Date(info.created_at), + partitionIds: info.partition_ids, + }; + logger.verbose( + "[%s] The hub runtime info is: %O", + this._context.connectionId, + runtimeInfo + ); - clientSpan.setStatus({ code: SpanStatusCode.OK }); - return runtimeInfo; - } catch (error) { - clientSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - logger.warning( - `An error occurred while getting the hub runtime information: ${error?.name}: ${error?.message}` - ); - logErrorStackTrace(error); - throw error; - } finally { - clientSpan.end(); - } + return runtimeInfo; + } catch (error) { + logger.warning( + `An error occurred while getting the hub runtime information: ${error?.name}: ${error?.message}` + ); + logErrorStackTrace(error); + throw error; + } + }, + toSpanOptions(this._context.config) + ); } /** @@ -224,59 +222,55 @@ export class ManagementClient extends LinkEntity { ); partitionId = String(partitionId); - const { span: clientSpan } = createEventHubSpan( - "getPartitionProperties", + return tracingClient.withSpan( + "ManagementClient.getPartitionProperties", options, - this._context.config + async (updatedOptions) => { + try { + const securityToken = await this.getSecurityToken(); + const request: Message = { + body: Buffer.from(JSON.stringify([])), + message_id: uuid(), + reply_to: this.replyTo, + application_properties: { + operation: Constants.readOperation, + name: this.entityPath as string, + type: `${Constants.vendorString}:${Constants.partition}`, + partition: `${partitionId}`, + security_token: securityToken?.token, + }, + }; + + const info: any = await this._makeManagementRequest(request, { + ...updatedOptions, + requestName: "getPartitionInformation", + }); + + const partitionInfo: PartitionProperties = { + beginningSequenceNumber: info.begin_sequence_number, + eventHubName: info.name, + lastEnqueuedOffset: info.last_enqueued_offset, + lastEnqueuedOnUtc: new Date(info.last_enqueued_time_utc), + lastEnqueuedSequenceNumber: info.last_enqueued_sequence_number, + partitionId: info.partition, + isEmpty: info.is_partition_empty, + }; + logger.verbose( + "[%s] The partition info is: %O.", + this._context.connectionId, + partitionInfo + ); + return partitionInfo; + } catch (error) { + logger.warning( + `An error occurred while getting the partition information: ${error?.name}: ${error?.message}` + ); + logErrorStackTrace(error); + throw error; + } + }, + toSpanOptions(this._context.config) ); - - try { - const securityToken = await this.getSecurityToken(); - const request: Message = { - body: Buffer.from(JSON.stringify([])), - message_id: uuid(), - reply_to: this.replyTo, - application_properties: { - operation: Constants.readOperation, - name: this.entityPath as string, - type: `${Constants.vendorString}:${Constants.partition}`, - partition: `${partitionId}`, - security_token: securityToken?.token, - }, - }; - - const info: any = await this._makeManagementRequest(request, { - ...options, - requestName: "getPartitionInformation", - }); - - const partitionInfo: PartitionProperties = { - beginningSequenceNumber: info.begin_sequence_number, - eventHubName: info.name, - lastEnqueuedOffset: info.last_enqueued_offset, - lastEnqueuedOnUtc: new Date(info.last_enqueued_time_utc), - lastEnqueuedSequenceNumber: info.last_enqueued_sequence_number, - partitionId: info.partition, - isEmpty: info.is_partition_empty, - }; - logger.verbose("[%s] The partition info is: %O.", this._context.connectionId, partitionInfo); - - clientSpan.setStatus({ code: SpanStatusCode.OK }); - - return partitionInfo; - } catch (error) { - clientSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - logger.warning( - `An error occurred while getting the partition information: ${error?.name}: ${error?.message}` - ); - logErrorStackTrace(error); - throw error; - } finally { - clientSpan.end(); - } } /** diff --git a/sdk/eventhub/event-hubs/src/partitionPump.ts b/sdk/eventhub/event-hubs/src/partitionPump.ts index fe44c0992280..f3c7a9ce1262 100644 --- a/sdk/eventhub/event-hubs/src/partitionPump.ts +++ b/sdk/eventhub/event-hubs/src/partitionPump.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Link, Span, SpanKind, SpanStatusCode } from "@azure/core-tracing"; +import { TracingSpanOptions, TracingSpanLink } from "@azure/core-tracing"; import { logErrorStackTrace, logger } from "./log"; import { AbortController } from "@azure/abort-controller"; import { CloseReason } from "./models/public"; @@ -11,10 +11,9 @@ import { EventHubConnectionConfig } from "./eventhubConnectionConfig"; import { EventHubReceiver } from "./eventHubReceiver"; import { EventPosition } from "./eventPosition"; import { MessagingError } from "@azure/core-amqp"; -import { OperationOptions } from "./util/operationOptions"; import { PartitionProcessor } from "./partitionProcessor"; import { ReceivedEventData } from "./eventData"; -import { createEventHubSpan } from "./diagnostics/tracing"; +import { toSpanOptions, tracingClient } from "./diagnostics/tracing"; import { extractSpanContextFromEventData } from "./diagnostics/instrumentEventData"; /** @@ -131,13 +130,12 @@ export class PartitionPump { lastSeenSequenceNumber = receivedEvents[receivedEvents.length - 1].sequenceNumber; } - const span = createProcessingSpan( - receivedEvents, - this._context.config, - this._processorOptions + await tracingClient.withSpan( + "PartitionPump.process", + {}, + () => this._partitionProcessor.processEvents(receivedEvents), + toProcessingSpanOptions(receivedEvents, this._context.config) ); - - await trace(() => this._partitionProcessor.processEvents(receivedEvents), span); } catch (err) { // check if this pump is still receiving // it may not be if the EventProcessor was stopped during processEvents @@ -208,50 +206,25 @@ export class PartitionPump { /** * @internal */ -export function createProcessingSpan( +export function toProcessingSpanOptions( receivedEvents: ReceivedEventData[], - eventHubProperties: Pick, - options?: OperationOptions -): Span { - const links: Link[] = []; - + eventHubProperties: Pick +): TracingSpanOptions { + const spanLinks: TracingSpanLink[] = []; for (const receivedEvent of receivedEvents) { - const spanContext = extractSpanContextFromEventData(receivedEvent); - - if (spanContext == null) { - continue; + const tracingContext = extractSpanContextFromEventData(receivedEvent); + if (tracingContext) { + spanLinks.push({ + tracingContext, + attributes: { + enqueuedTime: receivedEvent.enqueuedTimeUtc.getTime(), + }, + }); } - - links.push({ - context: spanContext, - attributes: { - enqueuedTime: receivedEvent.enqueuedTimeUtc.getTime(), - }, - }); - } - - const { span } = createEventHubSpan("process", options, eventHubProperties, { - kind: SpanKind.CONSUMER, - links, - }); - - return span; -} - -/** - * @internal - */ -export async function trace(fn: () => Promise, span: Span): Promise { - try { - await fn(); - span.setStatus({ code: SpanStatusCode.OK }); - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, - }); - throw err; - } finally { - span.end(); } + return { + spanLinks, + spanKind: "consumer", + ...toSpanOptions(eventHubProperties), + }; } diff --git a/sdk/eventhub/event-hubs/test/internal/diagnostics/messageSpan.spec.ts b/sdk/eventhub/event-hubs/test/internal/diagnostics/messageSpan.spec.ts deleted file mode 100644 index 1d859a6ed0da..000000000000 --- a/sdk/eventhub/event-hubs/test/internal/diagnostics/messageSpan.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { resetTracer, setTracer } from "@azure/test-utils"; -import chai from "chai"; -import { createMessageSpan } from "../../../src/diagnostics/tracing"; -import { testWithServiceTypes } from "../../public/utils/testWithServiceTypes"; - -const should = chai.should(); -const assert = chai.assert; - -testWithServiceTypes(() => { - describe("#createMessageSpan()", () => { - before(() => { - setTracer(); - }); - - after(() => { - resetTracer(); - }); - - it("should create a span without a parent", () => { - const { span } = createMessageSpan( - {}, - { - entityPath: "entity path", - host: "host", - } - ); - - should.exist(span); - should.exist(span.spanContext().spanId); - should.exist(span.spanContext().traceId); - - should.equal((span as any).name, "Azure.EventHubs.message"); - assert.deepStrictEqual((span as any).attributes, { - "az.namespace": "Microsoft.EventHub", - "message_bus.destination": "entity path", - "peer.address": "host", - }); - - span.end(); - }); - }); -}); diff --git a/sdk/eventhub/event-hubs/test/internal/diagnostics/tracing.spec.ts b/sdk/eventhub/event-hubs/test/internal/diagnostics/tracing.spec.ts new file mode 100644 index 000000000000..201479287c9a --- /dev/null +++ b/sdk/eventhub/event-hubs/test/internal/diagnostics/tracing.spec.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import { assert, MockInstrumenter, MockTracingSpan } from "@azure/test-utils"; +import { + instrumentEventData, + TRACEPARENT_PROPERTY, +} from "../../../src/diagnostics/instrumentEventData"; +import { toSpanOptions, tracingClient } from "../../../src/diagnostics/tracing"; +import Sinon from "sinon"; + +describe("tracing", () => { + describe("#getAdditionalSpanOptions", () => { + it("returns the initial set of attributes", () => { + assert.deepEqual(toSpanOptions({ entityPath: "testPath", host: "testHost" }), { + spanAttributes: { + "message_bus.destination": "testPath", + "peer.address": "testHost", + }, + }); + }); + + it("sets the spanKind if provided", () => { + const expectedSpanKind = "client"; + assert.equal( + toSpanOptions({ entityPath: "", host: "" }, expectedSpanKind).spanKind, + expectedSpanKind + ); + }); + }); + + describe("#instrumentEventData", () => { + afterEach(() => { + Sinon.restore(); + }); + + it("is idempotent", () => { + const tracingClientSpy = Sinon.spy(tracingClient, "startSpan"); + const instrumentedEventData = { + body: "test", + properties: { + [TRACEPARENT_PROPERTY]: "exists", + }, + }; + const { event, spanContext } = instrumentEventData( + instrumentedEventData, + {}, + "testPath", + "testHost" + ); + assert.notExists(spanContext); + assert.equal(event.properties?.[TRACEPARENT_PROPERTY], "exists"); + assert.equal(tracingClientSpy.callCount, 0); + }); + + it("returns early if the span is not recording", () => { + const instrumenter = new MockInstrumenter(); + const { span: nonRecordingSpan } = instrumenter.startSpan("test"); + (nonRecordingSpan as MockTracingSpan).setIsRecording(false); + // Setup our tracingClient to ensure we reach the happy path. + Sinon.stub(tracingClient, "startSpan").returns({ + span: nonRecordingSpan, + updatedOptions: {}, + }); + const { event, spanContext } = instrumentEventData({ body: "" }, {}, "testPath", "testHost"); + assert.notExists(spanContext); // was not instrumented + assert.notExists(event.properties?.[TRACEPARENT_PROPERTY]); + }); + + describe("when the span is valid", () => { + it("sets the traceparent on eventData", () => { + const instrumenter = new MockInstrumenter(); + const { span: recordingSpan } = instrumenter.startSpan("test"); + (recordingSpan as MockTracingSpan).setIsRecording(true); + + // Setup our tracingClient to ensure we reach the happy path. + Sinon.stub(tracingClient, "startSpan").returns({ + span: recordingSpan, + updatedOptions: {}, + }); + Sinon.stub(tracingClient, "createRequestHeaders").returns({ + traceparent: "fake-traceparent-header", + }); + + const { event } = instrumentEventData({ body: "test" }, {}, "testPath", "testHost"); + + assert.equal(event.properties?.[TRACEPARENT_PROPERTY], "fake-traceparent-header"); + }); + }); + }); +}); diff --git a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts index 8fd955161266..e45f7312fc32 100644 --- a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts @@ -15,13 +15,14 @@ import { extractSpanContextFromEventData, } from "../../src/diagnostics/instrumentEventData"; import { SubscriptionHandlerForTests } from "../public/utils/subscriptionHandlerForTests"; -import { TraceFlags } from "@azure/core-tracing"; import chai, { assert } from "chai"; import chaiAsPromised from "chai-as-promised"; import { createMockServer } from "../public/utils/mockService"; import debugModule from "debug"; import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; import { v4 as uuid } from "uuid"; +import { tracingClient } from "../../src/diagnostics/tracing"; +import Sinon from "sinon"; const should = chai.should(); chai.use(chaiAsPromised); @@ -399,10 +400,9 @@ testWithServiceTypes((serviceVersion) => { }).timeout(60000); describe("extractSpanContextFromEventData", function () { - it("should extract a SpanContext from a properly instrumented EventData", function () { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; + it("should use diagnostic Id from a properly instrumented EventData", function () { + const tracingClientSpy = Sinon.spy(tracingClient, "parseTraceparentHeader"); + const traceparent = `00-11111111111111111111111111111111-2222222222222222-00`; const eventData: ReceivedEventData = { body: "This is a test.", enqueuedTimeUtc: new Date(), @@ -410,49 +410,14 @@ testWithServiceTypes((serviceVersion) => { sequenceNumber: 0, partitionKey: null, properties: { - [TRACEPARENT_PROPERTY]: `00-${traceId}-${spanId}-${flags}`, + [TRACEPARENT_PROPERTY]: traceparent, }, getRawAmqpMessage() { return {} as any; }, }; - - const spanContext = extractSpanContextFromEventData(eventData); - - should.exist(spanContext, "Extracted spanContext should be defined."); - should.equal(spanContext!.traceId, traceId, "Extracted traceId does not match expectation."); - should.equal(spanContext!.spanId, spanId, "Extracted spanId does not match expectation."); - should.equal( - spanContext!.traceFlags, - TraceFlags.NONE, - "Extracted traceFlags do not match expectations." - ); - }); - - it("should return undefined when EventData is not properly instrumented", function () { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; - const eventData: ReceivedEventData = { - body: "This is a test.", - enqueuedTimeUtc: new Date(), - offset: 0, - sequenceNumber: 0, - partitionKey: null, - properties: { - [TRACEPARENT_PROPERTY]: `99-${traceId}-${spanId}-${flags}`, - }, - getRawAmqpMessage() { - return {} as any; - }, - }; - - const spanContext = extractSpanContextFromEventData(eventData); - - should.not.exist( - spanContext, - "Invalid diagnosticId version should return undefined spanContext." - ); + extractSpanContextFromEventData(eventData); + assert.isTrue(tracingClientSpy.calledWith(traceparent)); }); it("should return undefined when EventData is not instrumented", function () { diff --git a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts index 413d198094c5..282e79fd2b03 100644 --- a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts @@ -1,164 +1,61 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - Context, - SpanKind, - SpanOptions, - SpanStatusCode, - context, - setSpanContext, -} from "@azure/core-tracing"; -import { TestSpan, TestTracer } from "@azure/test-utils"; -import { createProcessingSpan, trace } from "../../src/partitionPump"; -import { ReceivedEventData } from "../../src/eventData"; -import chai from "chai"; -import { instrumentEventData } from "../../src/diagnostics/instrumentEventData"; -import { setTracerForTest } from "../public/utils/testUtils"; +import { assert } from "@azure/test-utils"; import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; - -const should = chai.should(); +import { toProcessingSpanOptions } from "../../src/partitionPump"; +import Sinon from "sinon"; +import { tracingClient } from "../../src/diagnostics/tracing"; +import { TracingContext } from "@azure/core-tracing"; +import { TRACEPARENT_PROPERTY } from "../../src/diagnostics/instrumentEventData"; testWithServiceTypes(() => { describe("PartitionPump", () => { + afterEach(() => { + Sinon.restore(); + }); describe("telemetry", () => { - const eventHubProperties = { - host: "thehost", - entityPath: "theeventhubname", - }; - - class TestTracer2 extends TestTracer { - public spanOptions: SpanOptions | undefined; - public spanName: string | undefined; - public context: Context | undefined; - - startSpan(nameArg: string, optionsArg?: SpanOptions, contextArg?: Context): TestSpan { - this.spanName = nameArg; - this.spanOptions = optionsArg; - this.context = contextArg; - return super.startSpan(nameArg, optionsArg, this.context); - } - } - - it("basic span properties are set", async () => { - const { tracer, resetTracer } = setTracerForTest(new TestTracer2()); - const fakeParentSpanContext = setSpanContext( - context.active(), - tracer.startSpan("test").spanContext() - ); - - await createProcessingSpan([], eventHubProperties, { - tracingOptions: { - tracingContext: fakeParentSpanContext, - }, - }); - - should.equal(tracer.spanName, "Azure.EventHubs.process"); - - should.exist(tracer.spanOptions); - tracer.spanOptions!.kind!.should.equal(SpanKind.CONSUMER); - tracer.context!.should.equal(fakeParentSpanContext); - - const attributes = tracer - .getActiveSpans() - .find((s) => s.name === "Azure.EventHubs.process")?.attributes; - - attributes!.should.deep.equal({ - "az.namespace": "Microsoft.EventHub", - "message_bus.destination": eventHubProperties.entityPath, - "peer.address": eventHubProperties.host, + describe("#getProcessingSpanOptions", () => { + it("returns basic span properties", () => { + const processingSpanOptions = toProcessingSpanOptions([], { + entityPath: "testPath", + host: "testHost", + }); + assert.equal(processingSpanOptions.spanKind, "consumer"); + assert.deepEqual(processingSpanOptions.spanAttributes, { + "message_bus.destination": "testPath", + "peer.address": "testHost", + }); }); - resetTracer(); - }); - - it("received events are linked to this span using Diagnostic-Id", async () => { - const requiredEventProperties = { - body: "", - enqueuedTimeUtc: new Date(), - offset: 0, - partitionKey: null, - sequenceNumber: 0, - getRawAmqpMessage() { - return {} as any; - }, - }; - - const { tracer, resetTracer } = setTracerForTest(new TestTracer2()); - - const firstEvent = tracer.startSpan("a"); - const thirdEvent = tracer.startSpan("c"); - - const receivedEvents: ReceivedEventData[] = [ - instrumentEventData( - { ...requiredEventProperties }, - { - tracingOptions: { - tracingContext: setSpanContext(context.active(), firstEvent.spanContext()), - }, + it("creates spanLinks correctly", () => { + const enqueuedTimeUtc = new Date(); + const requiredEventProperties = { + body: "", + enqueuedTimeUtc, + offset: 0, + partitionKey: null, + sequenceNumber: 0, + properties: { + [TRACEPARENT_PROPERTY]: "test", }, - "entityPath", - "host" - ).event as ReceivedEventData, - { properties: {}, ...requiredEventProperties }, // no diagnostic ID means it gets skipped - instrumentEventData( - { ...requiredEventProperties }, - { - tracingOptions: { - tracingContext: setSpanContext(context.active(), thirdEvent.spanContext()), - }, + getRawAmqpMessage() { + return {} as any; }, - "entityPath", - "host" - ).event as ReceivedEventData, - ]; - - await createProcessingSpan(receivedEvents, eventHubProperties, {}); - - // middle event, since it has no trace information, doesn't get included - // in the telemetry - tracer.spanOptions!.links!.length.should.equal(3 - 1); - // the test tracer just hands out a string integer that just gets - // incremented - tracer.spanOptions!.links![0]!.context.traceId.should.equal( - firstEvent.spanContext().traceId - ); - (tracer.spanOptions!.links![0]!.attributes!.enqueuedTime as number).should.equal( - requiredEventProperties.enqueuedTimeUtc.getTime() - ); - tracer.spanOptions!.links![1]!.context.traceId.should.equal( - thirdEvent.spanContext().traceId - ); - (tracer.spanOptions!.links![1]!.attributes!.enqueuedTime as number).should.equal( - requiredEventProperties.enqueuedTimeUtc.getTime() - ); - - resetTracer(); - }); - - it("trace - normal", async () => { - const tracer = new TestTracer(); - const span = tracer.startSpan("whatever"); - - await trace(async () => { - /* no-op */ - }, span); - - span.status!.code.should.equal(SpanStatusCode.OK); - should.equal(span.endCalled, true); - }); - - it("trace - throws", async () => { - const tracer = new TestTracer(); - const span = tracer.startSpan("whatever"); - - await trace(async () => { - throw new Error("error thrown from fn"); - }, span).should.be.rejectedWith(/error thrown from fn/); - - span.status!.code.should.equal(SpanStatusCode.ERROR); - span.status!.message!.should.equal("error thrown from fn"); - should.equal(span.endCalled, true); + }; + const fakeContext = {} as TracingContext; + Sinon.stub(tracingClient, "parseTraceparentHeader").returns(fakeContext); + + const processingSpanOptions = toProcessingSpanOptions([requiredEventProperties], { + entityPath: "testPath", + host: "testHost", + }); + + assert.lengthOf(processingSpanOptions.spanLinks!, 1); + const spanLink = processingSpanOptions.spanLinks![0]; + assert.equal(spanLink.attributes!["enqueuedTime"], enqueuedTimeUtc.getTime()); + assert.equal(spanLink!.tracingContext, fakeContext); + }); }); }); }); diff --git a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts index 8cc99d4c44d5..5ae991312254 100644 --- a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts @@ -1,26 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - EnvVarKeys, - getEnvVars, - getStartingPositionsForTests, - setTracerForTest, -} from "../public/utils/testUtils"; +import { EnvVarKeys, getEnvVars, getStartingPositionsForTests } from "../public/utils/testUtils"; import { EventData, EventHubConsumerClient, EventHubProducerClient, EventPosition, - OperationOptions, ReceivedEventData, SendBatchOptions, - TryAddOptions, } from "../../src"; -import { SpanGraph, TestSpan } from "@azure/test-utils"; -import { context, setSpan } from "@azure/core-tracing"; +import { assert } from "@azure/test-utils"; import { SubscriptionHandlerForTests } from "../public/utils/subscriptionHandlerForTests"; -import { TRACEPARENT_PROPERTY } from "../../src/diagnostics/instrumentEventData"; import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { createMockServer } from "../public/utils/mockService"; @@ -321,59 +312,26 @@ testWithServiceTypes((serviceVersion) => { }); it("can be manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - const list = [{ name: "Albert" }, { name: "Marie" }]; - const eventDataBatch = await producerClient.createBatch({ - partitionId: "0", - }); + await assert.supportsTracing( + async (options) => { + const eventDataBatch = await producerClient.createBatch({ + partitionId: "0", + tracingOptions: options.tracingOptions, + }); - for (let i = 0; i < 2; i++) { - eventDataBatch.tryAdd( - { body: `${list[i].name}` }, - { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd({ body: `${list[i].name}` }, options); } - ); - } - await producerClient.sendBatch(eventDataBatch); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(2, "Should only have two root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - resetTracer(); + return producerClient.sendBatch(eventDataBatch, options); + }, + ["message", "EventHubProducerClient.sendBatch"] + ); }); it("doesn't create empty spans when tracing is disabled", async () => { const events: EventData[] = [{ body: "foo" }, { body: "bar" }]; - const eventDataBatch = await producerClient.createBatch(); for (const event of events) { @@ -388,139 +346,38 @@ testWithServiceTypes((serviceVersion) => { ); }); - function legacyOptionsUsingSpanContext( - rootSpan: TestSpan - ): Pick { - return { - parentSpan: rootSpan.spanContext(), - }; - } - - function legacyOptionsUsingSpan(rootSpan: TestSpan): Pick { - return { - parentSpan: rootSpan, - }; - } + it("supports tracing", async () => { + const list = [{ name: "Albert" }, { name: "Marie" }]; + const eventDataBatch = await producerClient.createBatch({ + partitionId: "0", + }); - function modernOptions(rootSpan: TestSpan): OperationOptions { - return { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), + await assert.supportsTracing( + (options) => { + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd({ body: `${list[i].name}` }, options); + } + return producerClient.sendBatch(eventDataBatch, options); }, - }; - } - - [legacyOptionsUsingSpan, legacyOptionsUsingSpanContext, modernOptions].forEach( - (optionsFn) => { - describe(`tracing (${optionsFn.name})`, () => { - it("will not instrument already instrumented events", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("test"); - - const list = [ - { name: "Albert" }, - { - name: "Marie", - properties: { - [TRACEPARENT_PROPERTY]: "foo", - }, - }, - ]; - - const eventDataBatch = await producerClient.createBatch({ - partitionId: "0", - }); - - for (let i = 0; i < 2; i++) { - eventDataBatch.tryAdd( - { body: `${list[i].name}`, properties: list[i].properties }, - optionsFn(rootSpan) - ); - } - await producerClient.sendBatch(eventDataBatch); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(2, "Should only have two root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer - .getActiveSpans() - .length.should.equal(0, "All spans should have had end called."); - resetTracer(); - }); - - it("will support tracing batch and send", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - - const list = [{ name: "Albert" }, { name: "Marie" }]; + ["message", "EventHubProducerClient.sendBatch"] + ); + }); - const eventDataBatch = await producerClient.createBatch({ - partitionId: "0", - }); - for (let i = 0; i < 2; i++) { - eventDataBatch.tryAdd({ body: `${list[i].name}` }, optionsFn(rootSpan)); - } - await producerClient.sendBatch(eventDataBatch, { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root span."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.send", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer - .getActiveSpans() - .length.should.equal(0, "All spans should have had end called."); - resetTracer(); - }); - }); + it("supports tracing multiple events", async () => { + const events: EventData[] = []; + for (let i = 0; i < 5; i++) { + events.push({ body: `multiple messages - manual trace propgation: ${i}` }); } - ); + + await assert.supportsTracing( + (options) => + producerClient.sendBatch(events, { + partitionId: "0", + tracingOptions: options.tracingOptions, + }), + ["message", "EventHubProducerClient.sendBatch"] + ); + }); it("with partition key should be sent successfully.", async function (): Promise { const eventDataBatch = await producerClient.createBatch({ partitionKey: "1" }); @@ -648,623 +505,379 @@ testWithServiceTypes((serviceVersion) => { debug("Sent the message successfully on the same link.."); }); - it("can be manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); + describe("Array of events", function () { + it("should be sent successfully", async () => { + const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; + const receivedEvents: ReceivedEventData[] = []; + let receivingResolver: (value?: unknown) => void; - const events = []; - for (let i = 0; i < 5; i++) { - events.push({ body: `multiple messages - manual trace propgation: ${i}` }); - } - await producerClient.sendBatch(events, { - partitionId: "0", - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ + const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); + const subscription = consumerClient.subscribe( { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.send", - children: [], - }, - ], + async processError() { + /* no-op */ + }, + async processEvents(events) { + receivedEvents.push(...events); + receivingResolver(); + }, }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - - resetTracer(); - }); - - it("skips already instrumented events when manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - - const events: EventData[] = []; - for (let i = 0; i < 5; i++) { - events.push({ body: `multiple messages - manual trace propgation: ${i}` }); - } - events[0].properties = { [TRACEPARENT_PROPERTY]: "foo" }; - await producerClient.sendBatch(events, { - partitionId: "0", - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.send", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - - resetTracer(); - }); - }); - - describe("Array of events", function () { - it("should be sent successfully", async () => { - const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; - const receivedEvents: ReceivedEventData[] = []; - let receivingResolver: (value?: unknown) => void; - - const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); - const subscription = consumerClient.subscribe( - { - async processError() { - /* no-op */ - }, - async processEvents(events) { - receivedEvents.push(...events); - receivingResolver(); - }, - }, - { - startPosition, - maxBatchSize: data.length, - } - ); - - await producerClient.sendBatch(data); - - await receivingPromise; - await subscription.close(); - - receivedEvents.length.should.equal(data.length); - receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); - }); - - it("should be sent successfully with partitionKey", async () => { - const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; - const receivedEvents: ReceivedEventData[] = []; - let receivingResolver: (value?: unknown) => void; - const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); - const subscription = consumerClient.subscribe( - { - async processError() { - /* no-op */ - }, - async processEvents(events) { - receivedEvents.push(...events); - receivingResolver(); - }, - }, - { - startPosition, - maxBatchSize: data.length, - } - ); - - await producerClient.sendBatch(data, { partitionKey: "foo" }); - - await receivingPromise; - await subscription.close(); - - receivedEvents.length.should.equal(data.length); - receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); - for (let i = 0; i < receivedEvents.length; i++) { - receivedEvents[i].body.should.equal(data[i].body); - } - }); - - it("should be sent successfully with partitionId", async () => { - const partitionId = "0"; - const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; - const receivedEvents: ReceivedEventData[] = []; - let receivingResolver: (value?: unknown) => void; - const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); - const subscription = consumerClient.subscribe( - partitionId, - { - async processError() { - /* no-op */ - }, - async processEvents(events) { - receivedEvents.push(...events); - receivingResolver(); - }, - }, - { - startPosition, - maxBatchSize: data.length, - } - ); - - await producerClient.sendBatch(data, { partitionId }); - - await receivingPromise; - await subscription.close(); - - receivedEvents.length.should.equal(data.length); - receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); - for (let i = 0; i < receivedEvents.length; i++) { - receivedEvents[i].body.should.equal(data[i].body); - } - }); + startPosition, + maxBatchSize: data.length, + } + ); - it("can be manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); + await producerClient.sendBatch(data); - const rootSpan = tracer.startSpan("root"); + await receivingPromise; + await subscription.close(); - const events = []; - for (let i = 0; i < 5; i++) { - events.push({ body: `multiple messages - manual trace propgation: ${i}` }); - } - await producerClient.sendBatch(events, { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, + receivedEvents.length.should.equal(data.length); + receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); }); - rootSpan.end(); - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ + it("should be sent successfully with partitionKey", async () => { + const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; + const receivedEvents: ReceivedEventData[] = []; + let receivingResolver: (value?: unknown) => void; + const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); + const subscription = consumerClient.subscribe( { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.send", - children: [], - }, - ], + async processError() { + /* no-op */ + }, + async processEvents(events) { + receivedEvents.push(...events); + receivingResolver(); + }, }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - - const knownSendSpans = tracer - .getKnownSpans() - .filter((span: TestSpan) => span.name === "Azure.EventHubs.send"); - knownSendSpans.length.should.equal(1, "There should have been one send span."); - knownSendSpans[0].attributes.should.deep.equal({ - "az.namespace": "Microsoft.EventHub", - "message_bus.destination": producerClient.eventHubName, - "peer.address": producerClient.fullyQualifiedNamespace, - }); - resetTracer(); - }); + { + startPosition, + maxBatchSize: data.length, + } + ); - it("skips already instrumented events when manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); + await producerClient.sendBatch(data, { partitionKey: "foo" }); - const rootSpan = tracer.startSpan("root"); + await receivingPromise; + await subscription.close(); - const events: EventData[] = []; - for (let i = 0; i < 5; i++) { - events.push({ body: `multiple messages - manual trace propgation: ${i}` }); - } - events[0].properties = { [TRACEPARENT_PROPERTY]: "foo" }; - await producerClient.sendBatch(events, { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, + receivedEvents.length.should.equal(data.length); + receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); + for (let i = 0; i < receivedEvents.length; i++) { + receivedEvents[i].body.should.equal(data[i].body); + } }); - rootSpan.end(); - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root spans."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ + it("should be sent successfully with partitionId", async () => { + const partitionId = "0"; + const data: EventData[] = [{ body: "Hello World 1" }, { body: "Hello World 2" }]; + const receivedEvents: ReceivedEventData[] = []; + let receivingResolver: (value?: unknown) => void; + const receivingPromise = new Promise((resolve) => (receivingResolver = resolve)); + const subscription = consumerClient.subscribe( + partitionId, { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.message", - children: [], - }, - { - name: "Azure.EventHubs.send", - children: [], - }, - ], + async processError() { + /* no-op */ + }, + async processEvents(events) { + receivedEvents.push(...events); + receivingResolver(); + }, }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - resetTracer(); - }); - - it("should throw when partitionId and partitionKey are provided", async function (): Promise { - try { - const data: EventData[] = [ { - body: "Sender paritition id and partition key", - }, - ]; - await producerClient.sendBatch(data, { partitionKey: "1", partitionId: "0" }); - throw new Error("Test Failure"); - } catch (err) { - err.message.should.equal( - "The partitionId (0) and partitionKey (1) cannot both be specified." + startPosition, + maxBatchSize: data.length, + } ); - } - }); - }); - describe("Validation", function () { - describe("createBatch", function () { - it("throws an error if partitionId and partitionKey are set", async () => { - try { - await producerClient.createBatch({ partitionId: "0", partitionKey: "boo" }); - throw new Error("Test failure"); - } catch (error) { - error.message.should.equal( - "The partitionId (0) and partitionKey (boo) cannot both be specified." - ); - } - }); + await producerClient.sendBatch(data, { partitionId }); - it("throws an error if partitionId and partitionKey are set and partitionId is 0 i.e. falsy", async () => { - try { - await producerClient.createBatch({ - // @ts-expect-error Testing the value 0 is not ignored. - partitionId: 0, - partitionKey: "boo", - }); - throw new Error("Test failure"); - } catch (error) { - error.message.should.equal( - "The partitionId (0) and partitionKey (boo) cannot both be specified." - ); - } - }); + await receivingPromise; + await subscription.close(); - it("throws an error if partitionId and partitionKey are set and partitionKey is 0 i.e. falsy", async () => { - try { - await producerClient.createBatch({ - partitionId: "1", - // @ts-expect-error Testing the value 0 is not ignored. - partitionKey: 0, - }); - throw new Error("Test failure"); - } catch (error) { - error.message.should.equal( - "The partitionId (1) and partitionKey (0) cannot both be specified." - ); + receivedEvents.length.should.equal(data.length); + receivedEvents.map((e) => e.body).should.eql(data.map((d) => d.body)); + for (let i = 0; i < receivedEvents.length; i++) { + receivedEvents[i].body.should.equal(data[i].body); } }); - it("should throw when maxMessageSize is greater than maximum message size on the AMQP sender link", async function (): Promise { + it("should throw when partitionId and partitionKey are provided", async function (): Promise { try { - await producerClient.createBatch({ maxSizeInBytes: 2046528 }); + const data: EventData[] = [ + { + body: "Sender paritition id and partition key", + }, + ]; + await producerClient.sendBatch(data, { partitionKey: "1", partitionId: "0" }); throw new Error("Test Failure"); - } catch (err) { - err.message.should.match( - /.*Max message size \((\d+) bytes\) is greater than maximum message size \((\d+) bytes\) on the AMQP sender link.*/gi - ); - } - }); - }); - describe("sendBatch with EventDataBatch", function () { - it("works if partitionKeys match", async () => { - const misconfiguredOptions: SendBatchOptions = { - partitionKey: "foo", - }; - const batch = await producerClient.createBatch({ partitionKey: "foo" }); - await producerClient.sendBatch(batch, misconfiguredOptions); - }); - it("works if partitionIds match", async () => { - const misconfiguredOptions: SendBatchOptions = { - partitionId: "0", - }; - const batch = await producerClient.createBatch({ partitionId: "0" }); - await producerClient.sendBatch(batch, misconfiguredOptions); - }); - it("throws an error if partitionKeys don't match", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "bar", - }; - const batch = await producerClient.createBatch({ partitionKey: "foo" }); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.equal( - "The partitionKey (bar) set on sendBatch does not match the partitionKey (foo) set when creating the batch." - ); - } - }); - it("throws an error if partitionKeys don't match (undefined)", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "bar", - }; - const batch = await producerClient.createBatch(); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.equal( - "The partitionKey (bar) set on sendBatch does not match the partitionKey (undefined) set when creating the batch." - ); - } - }); - it("throws an error if partitionIds don't match", async () => { - const badOptions: SendBatchOptions = { - partitionId: "0", - }; - const batch = await producerClient.createBatch({ partitionId: "1" }); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); } catch (err) { err.message.should.equal( - "The partitionId (0) set on sendBatch does not match the partitionId (1) set when creating the batch." + "The partitionId (0) and partitionKey (1) cannot both be specified." ); } }); - it("throws an error if partitionIds don't match (undefined)", async () => { - const badOptions: SendBatchOptions = { - partitionId: "0", - }; - const batch = await producerClient.createBatch(); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.equal( - "The partitionId (0) set on sendBatch does not match the partitionId (undefined) set when creating the batch." - ); - } - }); - it("throws an error if partitionId and partitionKey are set (create, send)", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "foo", - }; - const batch = await producerClient.createBatch({ partitionId: "0" }); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.not.equal("Test failure"); - } + }); + + describe("Validation", function () { + describe("createBatch", function () { + it("throws an error if partitionId and partitionKey are set", async () => { + try { + await producerClient.createBatch({ partitionId: "0", partitionKey: "boo" }); + throw new Error("Test failure"); + } catch (error) { + error.message.should.equal( + "The partitionId (0) and partitionKey (boo) cannot both be specified." + ); + } + }); + + it("throws an error if partitionId and partitionKey are set and partitionId is 0 i.e. falsy", async () => { + try { + await producerClient.createBatch({ + // @ts-expect-error Testing the value 0 is not ignored. + partitionId: 0, + partitionKey: "boo", + }); + throw new Error("Test failure"); + } catch (error) { + error.message.should.equal( + "The partitionId (0) and partitionKey (boo) cannot both be specified." + ); + } + }); + + it("throws an error if partitionId and partitionKey are set and partitionKey is 0 i.e. falsy", async () => { + try { + await producerClient.createBatch({ + partitionId: "1", + // @ts-expect-error Testing the value 0 is not ignored. + partitionKey: 0, + }); + throw new Error("Test failure"); + } catch (error) { + error.message.should.equal( + "The partitionId (1) and partitionKey (0) cannot both be specified." + ); + } + }); + + it("should throw when maxMessageSize is greater than maximum message size on the AMQP sender link", async function (): Promise { + try { + await producerClient.createBatch({ maxSizeInBytes: 2046528 }); + throw new Error("Test Failure"); + } catch (err) { + err.message.should.match( + /.*Max message size \((\d+) bytes\) is greater than maximum message size \((\d+) bytes\) on the AMQP sender link.*/gi + ); + } + }); }); - it("throws an error if partitionId and partitionKey are set (send, create)", async () => { - const badOptions: SendBatchOptions = { - partitionId: "0", - }; - const batch = await producerClient.createBatch({ partitionKey: "foo" }); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.not.equal("Test failure"); - } + describe("sendBatch with EventDataBatch", function () { + it("works if partitionKeys match", async () => { + const misconfiguredOptions: SendBatchOptions = { + partitionKey: "foo", + }; + const batch = await producerClient.createBatch({ partitionKey: "foo" }); + await producerClient.sendBatch(batch, misconfiguredOptions); + }); + it("works if partitionIds match", async () => { + const misconfiguredOptions: SendBatchOptions = { + partitionId: "0", + }; + const batch = await producerClient.createBatch({ partitionId: "0" }); + await producerClient.sendBatch(batch, misconfiguredOptions); + }); + it("throws an error if partitionKeys don't match", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "bar", + }; + const batch = await producerClient.createBatch({ partitionKey: "foo" }); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionKey (bar) set on sendBatch does not match the partitionKey (foo) set when creating the batch." + ); + } + }); + it("throws an error if partitionKeys don't match (undefined)", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "bar", + }; + const batch = await producerClient.createBatch(); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionKey (bar) set on sendBatch does not match the partitionKey (undefined) set when creating the batch." + ); + } + }); + it("throws an error if partitionIds don't match", async () => { + const badOptions: SendBatchOptions = { + partitionId: "0", + }; + const batch = await producerClient.createBatch({ partitionId: "1" }); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionId (0) set on sendBatch does not match the partitionId (1) set when creating the batch." + ); + } + }); + it("throws an error if partitionIds don't match (undefined)", async () => { + const badOptions: SendBatchOptions = { + partitionId: "0", + }; + const batch = await producerClient.createBatch(); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionId (0) set on sendBatch does not match the partitionId (undefined) set when creating the batch." + ); + } + }); + it("throws an error if partitionId and partitionKey are set (create, send)", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "foo", + }; + const batch = await producerClient.createBatch({ partitionId: "0" }); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.not.equal("Test failure"); + } + }); + it("throws an error if partitionId and partitionKey are set (send, create)", async () => { + const badOptions: SendBatchOptions = { + partitionId: "0", + }; + const batch = await producerClient.createBatch({ partitionKey: "foo" }); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.not.equal("Test failure"); + } + }); + it("throws an error if partitionId and partitionKey are set (send, send)", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "foo", + partitionId: "0", + }; + const batch = await producerClient.createBatch(); + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.not.equal("Test failure"); + } + }); }); - it("throws an error if partitionId and partitionKey are set (send, send)", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "foo", - partitionId: "0", - }; - const batch = await producerClient.createBatch(); - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.not.equal("Test failure"); - } + + describe("sendBatch with EventDataBatch with events array", function () { + it("throws an error if partitionId and partitionKey are set", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "foo", + partitionId: "0", + }; + const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionId (0) and partitionKey (foo) cannot both be specified." + ); + } + }); + it("throws an error if partitionId and partitionKey are set with partitionId set to 0 i.e. falsy", async () => { + const badOptions: SendBatchOptions = { + partitionKey: "foo", + // @ts-expect-error Testing the value 0 is not ignored. + partitionId: 0, + }; + const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionId (0) and partitionKey (foo) cannot both be specified." + ); + } + }); + it("throws an error if partitionId and partitionKey are set with partitionKey set to 0 i.e. falsy", async () => { + const badOptions: SendBatchOptions = { + // @ts-expect-error Testing the value 0 is not ignored. + partitionKey: 0, + partitionId: "0", + }; + const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; + try { + await producerClient.sendBatch(batch, badOptions); + throw new Error("Test failure"); + } catch (err) { + err.message.should.equal( + "The partitionId (0) and partitionKey (0) cannot both be specified." + ); + } + }); }); }); - describe("sendBatch with EventDataBatch with events array", function () { - it("throws an error if partitionId and partitionKey are set", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "foo", - partitionId: "0", + describe("Negative scenarios", function (): void { + it("a message greater than 1 MB should fail.", async function (): Promise { + const data: EventData = { + body: Buffer.from("Z".repeat(1300000)), }; - const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; try { - await producerClient.sendBatch(batch, badOptions); + await producerClient.sendBatch([data]); throw new Error("Test failure"); } catch (err) { - err.message.should.equal( - "The partitionId (0) and partitionKey (foo) cannot both be specified." - ); - } - }); - it("throws an error if partitionId and partitionKey are set with partitionId set to 0 i.e. falsy", async () => { - const badOptions: SendBatchOptions = { - partitionKey: "foo", - // @ts-expect-error Testing the value 0 is not ignored. - partitionId: 0, - }; - const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.equal( - "The partitionId (0) and partitionKey (foo) cannot both be specified." - ); - } - }); - it("throws an error if partitionId and partitionKey are set with partitionKey set to 0 i.e. falsy", async () => { - const badOptions: SendBatchOptions = { - // @ts-expect-error Testing the value 0 is not ignored. - partitionKey: 0, - partitionId: "0", - }; - const batch = [{ body: "Hello 1" }, { body: "Hello 2" }]; - try { - await producerClient.sendBatch(batch, badOptions); - throw new Error("Test failure"); - } catch (err) { - err.message.should.equal( - "The partitionId (0) and partitionKey (0) cannot both be specified." + debug(err); + should.exist(err); + should.equal(err.code, "MessageTooLargeError"); + err.message.should.match( + /.*The received message \(delivery-id:(\d+), size:(\d+) bytes\) exceeds the limit \((\d+) bytes\) currently allowed on the link\..*/gi ); } }); - }); - }); - describe("Negative scenarios", function (): void { - it("a message greater than 1 MB should fail.", async function (): Promise { - const data: EventData = { - body: Buffer.from("Z".repeat(1300000)), - }; - try { - await producerClient.sendBatch([data]); - throw new Error("Test failure"); - } catch (err) { - debug(err); - should.exist(err); - should.equal(err.code, "MessageTooLargeError"); - err.message.should.match( - /.*The received message \(delivery-id:(\d+), size:(\d+) bytes\) exceeds the limit \((\d+) bytes\) currently allowed on the link\..*/gi - ); - } - }); - - describe("on invalid partition ids like", function (): void { - // tslint:disable-next-line: no-null-keyword - const invalidIds = ["XYZ", "-1", "1000", "-"]; - invalidIds.forEach(function (id: string | null): void { - it(`"${id}" should throw an error`, async function (): Promise { - try { - debug("Created sender and will be sending a message to partition id ...", id); - await producerClient.sendBatch([{ body: "Hello world!" }], { - partitionId: id as any, - }); - debug("sent the message."); - throw new Error("Test failure"); - } catch (err) { - debug(`>>>> Received error for invalid partition id "${id}" - `, err); - should.exist(err); - err.message.should.match( - /.*The specified partition is invalid for an EventHub partition sender or receiver.*/gi - ); - } + describe("on invalid partition ids like", function (): void { + // tslint:disable-next-line: no-null-keyword + const invalidIds = ["XYZ", "-1", "1000", "-"]; + invalidIds.forEach(function (id: string | null): void { + it(`"${id}" should throw an error`, async function (): Promise { + try { + debug("Created sender and will be sending a message to partition id ...", id); + await producerClient.sendBatch([{ body: "Hello world!" }], { + partitionId: id as any, + }); + debug("sent the message."); + throw new Error("Test failure"); + } catch (err) { + debug(`>>>> Received error for invalid partition id "${id}" - `, err); + should.exist(err); + err.message.should.match( + /.*The specified partition is invalid for an EventHub partition sender or receiver.*/gi + ); + } + }); }); }); }); - }); - }).timeout(20000); + }).timeout(20000); + }); }); diff --git a/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts b/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts index a80145d9b27b..6502866987c0 100644 --- a/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts +++ b/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { EnvVarKeys, getEnvVars, setTracerForTest } from "./utils/testUtils"; +import { EnvVarKeys, getEnvVars } from "./utils/testUtils"; import { EventHubBufferedProducerClient, EventHubConsumerClient, EventHubProducerClient, MessagingError, } from "../../src"; -import { context, setSpan } from "@azure/core-tracing"; -import { SpanGraph } from "@azure/test-utils"; +import { assert } from "@azure/test-utils"; import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { createMockServer } from "./utils/mockService"; @@ -106,38 +105,10 @@ testWithServiceTypes((serviceVersion) => { it("can be manually traced", async () => { const client = clientMap.get(clientType)!; - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - const ids = await client.getPartitionIds({ - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - ids.should.have.members(arrayOfIncreasingNumbersFromZero(ids.length)); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root span."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.getEventHubProperties", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - resetTracer(); + await assert.supportsTracing( + (options) => client.getPartitionIds(options), + ["ManagementClient.getEventHubProperties"] + ); }); }); @@ -155,40 +126,10 @@ testWithServiceTypes((serviceVersion) => { it("can be manually traced", async function (): Promise { const client = clientMap.get(clientType)!; - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - const hubRuntimeInfo = await client.getEventHubProperties({ - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - hubRuntimeInfo.partitionIds.should.have.members( - arrayOfIncreasingNumbersFromZero(hubRuntimeInfo.partitionIds.length) + await assert.supportsTracing( + (options) => client.getEventHubProperties(options), + ["ManagementClient.getEventHubProperties"] ); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root span."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.getEventHubProperties", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - resetTracer(); }); }); @@ -239,42 +180,10 @@ testWithServiceTypes((serviceVersion) => { it("can be manually traced", async () => { const client = clientMap.get(clientType)!; - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - const partitionRuntimeInfo = await client.getPartitionProperties("0", { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }); - partitionRuntimeInfo.partitionId.should.equal("0"); - partitionRuntimeInfo.eventHubName.should.equal(service.path); - partitionRuntimeInfo.lastEnqueuedOnUtc.should.be.instanceof(Date); - should.exist(partitionRuntimeInfo.lastEnqueuedSequenceNumber); - should.exist(partitionRuntimeInfo.lastEnqueuedOffset); - rootSpan.end(); - - const rootSpans = tracer.getRootSpans(); - rootSpans.length.should.equal(1, "Should only have one root span."); - rootSpans[0].should.equal(rootSpan, "The root span should match what was passed in."); - - const expectedGraph: SpanGraph = { - roots: [ - { - name: rootSpan.name, - children: [ - { - name: "Azure.EventHubs.getPartitionProperties", - children: [], - }, - ], - }, - ], - }; - - tracer.getSpanGraph(rootSpan.spanContext().traceId).should.eql(expectedGraph); - tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); - resetTracer(); + await assert.supportsTracing( + (options) => client.getPartitionProperties("0", options), + ["ManagementClient.getPartitionProperties"] + ); }); }); }); diff --git a/sdk/eventhub/event-hubs/test/public/node/client.spec.ts b/sdk/eventhub/event-hubs/test/public/node/client.spec.ts index 52dd68fd1309..048ff7d124d1 100644 --- a/sdk/eventhub/event-hubs/test/public/node/client.spec.ts +++ b/sdk/eventhub/event-hubs/test/public/node/client.spec.ts @@ -4,16 +4,15 @@ import { EnvVarKeys, getEnvVars } from "../utils/testUtils"; import { EnvironmentCredential, TokenCredential } from "@azure/identity"; import { EventHubConsumerClient, EventHubProducerClient } from "../../../src"; -import { TestTracer, resetTracer, setTracer } from "@azure/test-utils"; -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; +import { chai, assert, should as shouldFn } from "@azure/test-utils"; import chaiString from "chai-string"; import { createMockServer } from "../utils/mockService"; import { testWithServiceTypes } from "../utils/testWithServiceTypes"; +import Sinon from "sinon"; +import { tracingClient } from "../../../src/diagnostics/tracing"; -const should = chai.should(); -chai.use(chaiAsPromised); chai.use(chaiString); +const should = shouldFn(); testWithServiceTypes((serviceVersion) => { const env = getEnvVars(); @@ -24,14 +23,21 @@ testWithServiceTypes((serviceVersion) => { return service.start(); }); - after("Stopping mock service", () => { - return service?.stop(); + after("Stopping mock service", async () => { + await service?.stop(); }); } describe("Create clients using Azure Identity", function (): void { let endpoint: string; let credential: TokenCredential; + let client: EventHubConsumerClient | EventHubProducerClient; + + afterEach(async () => { + // The client must always be closed, or MockHub will hang on shutdown. + await client?.close(); + }); + before("validate environment", function () { should.exist( env[EnvVarKeys.AZURE_CLIENT_ID], @@ -64,18 +70,16 @@ testWithServiceTypes((serviceVersion) => { }); it("creates an EventHubProducerClient from an Azure.Identity credential", async function (): Promise { - const client = new EventHubProducerClient(endpoint, env.EVENTHUB_NAME, credential); + client = new EventHubProducerClient(endpoint, env.EVENTHUB_NAME, credential); should.equal(client.fullyQualifiedNamespace, endpoint); // Extra check involving actual call to the service to ensure this works const hubInfo = await client.getEventHubProperties(); should.equal(hubInfo.name, client.eventHubName); - - await client.close(); }); it("creates an EventHubConsumerClient from an Azure.Identity credential", async function (): Promise { - const client = new EventHubConsumerClient( + client = new EventHubConsumerClient( EventHubConsumerClient.defaultConsumerGroupName, endpoint, env.EVENTHUB_NAME, @@ -86,22 +90,11 @@ testWithServiceTypes((serviceVersion) => { // Extra check involving actual call to the service to ensure this works const hubInfo = await client.getEventHubProperties(); should.equal(hubInfo.name, client.eventHubName); - - await client.close(); }); describe("tracing", () => { - let tracer: TestTracer; - before(() => { - tracer = setTracer(); - }); - - after(() => { - resetTracer(); - }); - it("getEventHubProperties() creates a span with a peer.address attribute as the FQNS", async () => { - const client = new EventHubConsumerClient( + client = new EventHubConsumerClient( EventHubConsumerClient.defaultConsumerGroupName, endpoint, env.EVENTHUB_NAME, @@ -109,22 +102,30 @@ testWithServiceTypes((serviceVersion) => { ); should.equal(client.fullyQualifiedNamespace, endpoint); - // Extra check involving actual call to the service to ensure this works - const hubInfo = await client.getEventHubProperties(); - should.equal(hubInfo.name, client.eventHubName); + const withSpanStub = Sinon.spy(tracingClient, "withSpan"); - await client.close(); + // Ensure tracing is implemented correctly + await assert.supportsTracing( + (options) => client.getEventHubProperties(options), + ["ManagementClient.getEventHubProperties"] + ); - const spans = tracer - .getKnownSpans() - .filter((s) => s.name === "Azure.EventHubs.getEventHubProperties"); + // Additional validation that we created the correct initial span options + const expectedSpanOptions = { + spanAttributes: { + "peer.address": client.fullyQualifiedNamespace, + "message_bus.destination": client.eventHubName, + }, + }; - spans.length.should.equal(1); - spans[0].attributes.should.deep.equal({ - "az.namespace": "Microsoft.EventHub", - "message_bus.destination": client.eventHubName, - "peer.address": client.fullyQualifiedNamespace, - }); + assert.isTrue( + withSpanStub.calledWith( + Sinon.match.any, + Sinon.match.any, + Sinon.match.any, + expectedSpanOptions + ) + ); }); }); }); diff --git a/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts index 379b81ffb958..c1b2154846c8 100644 --- a/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts +++ b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts @@ -25,39 +25,44 @@ export async function supportsTracing< thisArg?: ThisParameterType ) { useInstrumenter(instrumenter); + instrumenter.enable(); instrumenter.reset(); - const startSpanOptions = { - packageName: "test", - ...options, - }; - const { span: rootSpan, tracingContext } = instrumenter.startSpan("root", startSpanOptions); + try { + const startSpanOptions = { + packageName: "test", + ...options, + }; + const { span: rootSpan, tracingContext } = instrumenter.startSpan("root", startSpanOptions); - const newOptions = { - ...options, - tracingOptions: { - tracingContext: tracingContext, - }, - } as Options; - await callback.call(thisArg, newOptions); - rootSpan.end(); - const spanGraph = getSpanGraph((rootSpan as MockTracingSpan).traceId, instrumenter); - assert.equal(spanGraph.roots.length, 1, "There should be just one root span"); - assert.equal(spanGraph.roots[0].name, "root"); - assert.strictEqual( - rootSpan, - instrumenter.startedSpans[0], - "The root span should match what was passed in." - ); + const newOptions = { + ...options, + tracingOptions: { + tracingContext: tracingContext, + }, + } as Options; + await callback.call(thisArg, newOptions); + rootSpan.end(); + const spanGraph = getSpanGraph((rootSpan as MockTracingSpan).traceId, instrumenter); + assert.equal(spanGraph.roots.length, 1, "There should be just one root span"); + assert.equal(spanGraph.roots[0].name, "root"); + assert.strictEqual( + rootSpan, + instrumenter.startedSpans[0], + "The root span should match what was passed in." + ); - const directChildren = spanGraph.roots[0].children.map((child) => child.name); - assert.sameMembers(Array.from(new Set(directChildren)), expectedSpanNames); - rootSpan.end(); - const openSpans = instrumenter.startedSpans.filter((s) => !s.endCalled); - assert.equal( - openSpans.length, - 0, - `All spans should have been closed, but found ${openSpans.map((s) => s.name)} open spans.` - ); + const directChildren = spanGraph.roots[0].children.map((child) => child.name); + assert.sameMembers(Array.from(new Set(directChildren)), expectedSpanNames); + rootSpan.end(); + const openSpans = instrumenter.startedSpans.filter((s) => !s.endCalled); + assert.equal( + openSpans.length, + 0, + `All spans should have been closed, but found ${openSpans.map((s) => s.name)} open spans.` + ); + } finally { + instrumenter.disable(); + } } /** diff --git a/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts index e7d55690ec0d..97a999793612 100644 --- a/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts +++ b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts @@ -14,6 +14,12 @@ import { MockTracingSpan } from "./mockTracingSpan"; * Represents an implementation of {@link Instrumenter} interface that keeps track of the tracing contexts and spans */ export class MockInstrumenter implements Instrumenter { + private isEnabled: boolean; + + constructor() { + this.isEnabled = false; + } + /** * Stack of immutable contexts, each of which is a bag of tracing values for the current operation */ @@ -58,7 +64,8 @@ export class MockInstrumenter implements Instrumenter { spanContext.traceId, spanContext.spanId, tracingContext, - spanOptions + spanOptions, + this.isEnabled ); let context: TracingContext = new MockContext(tracingContext); context = context.setValue(spanKey, span); @@ -107,4 +114,12 @@ export class MockInstrumenter implements Instrumenter { this.traceIdCounter = 0; this.spanIdCounter = 0; } + + disable() { + this.isEnabled = false; + } + + enable() { + this.isEnabled = true; + } } diff --git a/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts index 4ebcfd905f0b..554c9c2d7735 100644 --- a/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts +++ b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts @@ -37,6 +37,32 @@ export class MockTracingSpan implements TracingSpan { */ traceId: string; + /** + * The value passed to {@link TracingSpan.setStatus}, if any. + */ + spanStatus?: SpanStatus; + + /** + * All attributes recorded on the span. + */ + attributes: Record = {}; + + /** + * Value indictating wheher {@link TracingSpan.end} was called. + */ + endCalled = false; + + /** + * The exception captured on the span, if any. + */ + exception?: string | Error; + + /** + * Value indicating whether the span is recording. Used to test any + * early return when the span is not recording. + */ + private _isRecording: boolean; + /** * * @param name - Name of the current span @@ -49,19 +75,17 @@ export class MockTracingSpan implements TracingSpan { traceId: string, spanId: string, tracingContext?: TracingContext, - spanOptions?: TracingSpanOptions + spanOptions?: TracingSpanOptions, + enabled = true ) { this.name = name; this.spanKind = spanOptions?.spanKind; this.tracingContext = tracingContext; this.traceId = traceId; this.spanId = spanId; + this._isRecording = enabled; } - spanStatus?: SpanStatus; - attributes: Record = {}; - endCalled = false; - exception?: string | Error; setStatus(status: SpanStatus): void { this.spanStatus = status; } @@ -75,8 +99,12 @@ export class MockTracingSpan implements TracingSpan { this.exception = exception; } + setIsRecording(isRecording: boolean): void { + this._isRecording = isRecording; + } + isRecording(): boolean { - return true; + return this._isRecording; } parentSpan(): MockTracingSpan | undefined {