From ccbe774f6fb45050a2e8dee3293b117ae4c41526 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Tue, 16 Nov 2021 12:47:03 -0800 Subject: [PATCH 1/7] [core] - Implement the instrumenter and associated interfaces (#18658) - Add the following interfaces: - TracingContext - Instrumenter - InstrumenterSpanOptions - TracingClient + TracingClientOptions - TracingSpan, TracingSpanContext, TracingSpanKind, TracingSpanOptions - Add the following methods: - useInstrumenter - allows plugging in an instrumenter implementation at runtime - parseTraceparentHeader / createRequestHeaders - parse and stringify TracingSpan IDs - createTracingClient - Remove `@opentelemetry/api` from `@azure/core-tracing` This is a complete rewrite of `@azure/core-tracing` to provide the following benefits 1. Abstract away from OTel's API, using a smaller surface area that provides (what we think) is a nicer abstraction for managing tracing 2. Make tracing completely opt-in - without installing the (yet-to-be-created) instrumentation package and configuring it the user will not have to deal with any of OpenTelemetry's complexity 3. Remove `@opentelemetry/api` from all packages to support #17068 for RLCs Due to the large change I am working on a feature branch, so this is not being merged to `main` Resolves #18650 --- rush.json | 7 +- sdk/core/core-auth/review/core-auth.api.md | 15 +- sdk/core/core-auth/src/index.ts | 2 +- sdk/core/core-auth/src/tokenCredential.ts | 6 +- sdk/core/core-auth/src/tracing.ts | 6 +- sdk/core/core-tracing/package.json | 6 +- .../core-tracing/review/core-tracing.api.md | 203 ++---- sdk/core/core-tracing/src/createSpan.ts | 215 ------- sdk/core/core-tracing/src/index.ts | 47 +- sdk/core/core-tracing/src/instrumenter.ts | 77 +++ sdk/core/core-tracing/src/interfaces.ts | 597 ++++++------------ sdk/core/core-tracing/src/tracingClient.ts | 121 ++++ sdk/core/core-tracing/src/tracingContext.ts | 74 +++ .../src/utils/traceParentHeader.ts | 63 -- sdk/core/core-tracing/test/createSpan.spec.ts | 269 -------- .../core-tracing/test/instrumenter.spec.ts | 90 +++ sdk/core/core-tracing/test/interfaces.spec.ts | 91 +-- .../test/traceParentHeader.spec.ts | 125 ---- .../core-tracing/test/tracingClient.spec.ts | 197 ++++++ .../core-tracing/test/tracingContext.spec.ts | 121 ++++ .../test/util/testTracerProvider.ts | 25 - sdk/instrumentation/ci.yml | 30 + .../.nycrc | 19 + .../CHANGELOG.md | 3 + .../LICENSE | 21 + .../README.md | 94 +++ .../api-extractor.json | 31 + .../karma.conf.js | 130 ++++ .../package.json | 135 ++++ ...telemetry-instrumentation-azure-sdk.api.md | 23 + .../rollup.config.js | 3 + .../src/constants.ts} | 3 +- .../src/index.ts | 5 + .../src/instrumentation.browser.ts | 64 ++ .../src/instrumentation.ts | 81 +++ .../src/instrumenter.ts | 65 ++ .../src/logger.ts | 9 + .../src/spanWrapper.ts | 54 ++ .../src/transformations.ts | 95 +++ .../test/public/instrumenter.spec.ts | 244 +++++++ .../test/public/spanWrapper.spec.ts | 93 +++ .../test/public}/util/testSpan.ts | 44 +- .../test/public}/util/testTracer.ts | 26 +- .../test/public/util/testTracerProvider.ts | 41 ++ .../tests.yml | 11 + .../tsconfig.json | 11 + .../tsdoc.json | 4 + sdk/test-utils/test-utils/package.json | 4 +- sdk/test-utils/test-utils/src/index.ts | 6 +- .../test-utils/src/tracing/chaiAzureTrace.ts | 152 +++++ .../test-utils/src/tracing/mockContext.ts | 44 ++ .../src/tracing/mockInstrumenter.ts | 110 ++++ .../test-utils/src/tracing/mockTracingSpan.ts | 85 +++ .../test-utils/src/tracing/spanGraphModel.ts | 28 + .../test-utils/src/tracing/testSpan.ts | 19 +- .../test-utils/src/tracing/testTracer.ts | 67 +- .../src/tracing/testTracerProvider.ts | 30 +- .../test/tracing/mockInstrumenter.spec.ts | 130 ++++ .../test/tracing/mockTracingSpan.spec.ts | 37 ++ 59 files changed, 2946 insertions(+), 1462 deletions(-) delete mode 100644 sdk/core/core-tracing/src/createSpan.ts create mode 100644 sdk/core/core-tracing/src/instrumenter.ts create mode 100644 sdk/core/core-tracing/src/tracingClient.ts create mode 100644 sdk/core/core-tracing/src/tracingContext.ts delete mode 100644 sdk/core/core-tracing/src/utils/traceParentHeader.ts delete mode 100644 sdk/core/core-tracing/test/createSpan.spec.ts create mode 100644 sdk/core/core-tracing/test/instrumenter.spec.ts delete mode 100644 sdk/core/core-tracing/test/traceParentHeader.spec.ts create mode 100644 sdk/core/core-tracing/test/tracingClient.spec.ts create mode 100644 sdk/core/core-tracing/test/tracingContext.spec.ts delete mode 100644 sdk/core/core-tracing/test/util/testTracerProvider.ts create mode 100644 sdk/instrumentation/ci.yml create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js rename sdk/{core/core-tracing/src/utils/browser.d.ts => instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts} (53%) create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts rename sdk/{core/core-tracing/test => instrumentation/opentelemetry-instrumentation-azure-sdk/test/public}/util/testSpan.ts (84%) rename sdk/{core/core-tracing/test => instrumentation/opentelemetry-instrumentation-azure-sdk/test/public}/util/testTracer.ts (88%) create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json create mode 100644 sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockContext.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts create mode 100644 sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts create mode 100644 sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts diff --git a/rush.json b/rush.json index d974a5f3e38e..26aed024dec9 100644 --- a/rush.json +++ b/rush.json @@ -606,6 +606,11 @@ "projectFolder": "sdk/core/logger", "versionPolicyName": "core" }, + { + "packageName": "@azure/opentelemetry-instrumentation-azure-sdk", + "projectFolder": "sdk/instrumentation/opentelemetry-instrumentation-azure-sdk", + "versionPolicyName": "client" + }, { "packageName": "@azure/schema-registry", "projectFolder": "sdk/schemaregistry/schema-registry", @@ -1247,4 +1252,4 @@ "versionPolicyName": "management" } ] -} \ No newline at end of file +} diff --git a/sdk/core/core-auth/review/core-auth.api.md b/sdk/core/core-auth/review/core-auth.api.md index d477c52a7114..6f6c12e825f9 100644 --- a/sdk/core/core-auth/review/core-auth.api.md +++ b/sdk/core/core-auth/review/core-auth.api.md @@ -34,13 +34,6 @@ export class AzureSASCredential implements SASCredential { update(newSignature: string): void; } -// @public -export interface Context { - deleteValue(key: symbol): Context; - getValue(key: symbol): unknown; - setValue(key: symbol, value: unknown): Context; -} - // @public export interface GetTokenOptions { abortSignal?: AbortSignalLike; @@ -49,7 +42,7 @@ export interface GetTokenOptions { }; tenantId?: string; tracingOptions?: { - tracingContext?: Context; + tracingContext?: TracingContext; }; } @@ -83,6 +76,12 @@ export interface TokenCredential { getToken(scopes: string | string[], options?: GetTokenOptions): Promise; } +// @public +export interface TracingContext { + deleteValue(key: symbol): TracingContext; + getValue(key: symbol): unknown; + setValue(key: symbol, value: unknown): TracingContext; +} // (No @packageDocumentation comment for this package) diff --git a/sdk/core/core-auth/src/index.ts b/sdk/core/core-auth/src/index.ts index a44b072ce99e..d9345b49e750 100644 --- a/sdk/core/core-auth/src/index.ts +++ b/sdk/core/core-auth/src/index.ts @@ -16,4 +16,4 @@ export { isTokenCredential, } from "./tokenCredential"; -export { Context } from "./tracing"; +export { TracingContext } from "./tracing"; diff --git a/sdk/core/core-auth/src/tokenCredential.ts b/sdk/core/core-auth/src/tokenCredential.ts index ef8f0e04a3e6..2d8416b9cc1f 100644 --- a/sdk/core/core-auth/src/tokenCredential.ts +++ b/sdk/core/core-auth/src/tokenCredential.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { AbortSignalLike } from "@azure/abort-controller"; -import { Context } from "./tracing"; +import { TracingContext } from "./tracing"; /** * Represents a credential capable of providing an authentication token. @@ -43,9 +43,9 @@ export interface GetTokenOptions { */ tracingOptions?: { /** - * OpenTelemetry context + * Tracing Context for the current request. */ - tracingContext?: Context; + tracingContext?: TracingContext; }; /** diff --git a/sdk/core/core-auth/src/tracing.ts b/sdk/core/core-auth/src/tracing.ts index 3d8958fa8817..49a4cfe66cd7 100644 --- a/sdk/core/core-auth/src/tracing.ts +++ b/sdk/core/core-auth/src/tracing.ts @@ -7,7 +7,7 @@ /** * An interface structurally compatible with OpenTelemetry. */ -export interface Context { +export interface TracingContext { /** * Get a value from the context. * @@ -21,12 +21,12 @@ export interface Context { * @param key - context key for which to set the value * @param value - value to set for the given key */ - setValue(key: symbol, value: unknown): Context; + setValue(key: symbol, value: unknown): TracingContext; /** * Return a new context which inherits from this context but does * not contain a value for the given key. * * @param key - context key for which to clear a value */ - deleteValue(key: symbol): Context; + deleteValue(key: symbol): TracingContext; } diff --git a/sdk/core/core-tracing/package.json b/sdk/core/core-tracing/package.json index f5c12a00573b..8525b70d8fd6 100644 --- a/sdk/core/core-tracing/package.json +++ b/sdk/core/core-tracing/package.json @@ -5,9 +5,7 @@ "sdk-type": "client", "main": "dist/index.js", "module": "dist-esm/src/index.js", - "browser": { - "./dist-esm/src/utils/global.js": "./dist-esm/src/utils/global.browser.js" - }, + "browser": {}, "react-native": { "./dist/index.js": "./dist-esm/src/index.js" }, @@ -59,7 +57,6 @@ "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-tracing/README.md", "sideEffects": false, "dependencies": { - "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" }, "devDependencies": { @@ -67,7 +64,6 @@ "@azure/dev-tool": "^1.0.0", "@azure/eslint-plugin-azure-sdk": "^3.0.0", "@microsoft/api-extractor": "^7.18.11", - "@opentelemetry/tracing": "^0.22.0", "@types/chai": "^4.1.6", "@types/mocha": "^7.0.2", "@types/node": "^12.0.0", diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 82584f47f709..4b3b02b881a0 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -5,183 +5,100 @@ ```ts // @public -export interface Context { - deleteValue(key: symbol): Context; - getValue(key: symbol): unknown; - setValue(key: symbol, value: unknown): Context; -} - -// @public -const context_2: ContextAPI; -export { context_2 as context } +export function createTracingClient(options: TracingClientOptions): TracingClient; // @public -export interface ContextAPI { - active(): Context; +export interface Instrumenter { + createRequestHeaders(tracingContext?: TracingContext): Record; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + startSpan(name: string, spanOptions: InstrumenterSpanOptions): { + span: TracingSpan; + tracingContext: TracingContext; + }; + withContext ReturnType>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType; } // @public -export function createSpanFunction(args: CreateSpanFunctionArgs): (operationName: string, operationOptions?: T | undefined, startSpanOptions?: SpanOptions | undefined) => { - span: Span; - updatedOptions: T; -}; - -// @public -export interface CreateSpanFunctionArgs { - namespace: string; - packagePrefix: string; -} - -// @public -export type Exception = ExceptionWithCode | ExceptionWithMessage | ExceptionWithName | string; - -// @public -export interface ExceptionWithCode { - code: string | number; - message?: string; - name?: string; - stack?: string; -} - -// @public -export interface ExceptionWithMessage { - code?: string | number; - message: string; - name?: string; - stack?: string; -} - -// @public -export interface ExceptionWithName { - code?: string | number; - message?: string; - name: string; - stack?: string; -} - -// @public -export function extractSpanContextFromTraceParentHeader(traceParentHeader: string): SpanContext | undefined; - -// @public -export function getSpan(context: Context): Span | undefined; - -// @public -export function getSpanContext(context: Context): SpanContext | undefined; - -// @public -export function getTraceParentHeader(spanContext: SpanContext): string | undefined; - -// @public -export function getTracer(): Tracer; - -// @public -export function getTracer(name: string, version?: string): Tracer; - -// @public -export type HrTime = [number, number]; - -// @public -export function isSpanContextValid(context: SpanContext): boolean; - -// @public -export interface Link { - attributes?: SpanAttributes; - context: SpanContext; +export interface InstrumenterSpanOptions extends TracingSpanOptions { + packageName: string; + packageVersion?: string; + tracingContext?: TracingContext; } // @public export interface OperationTracingOptions { - tracingContext?: Context; -} - -// @public -export function setSpan(context: Context, span: Span): Context; - -// @public -export function setSpanContext(context: Context, spanContext: SpanContext): Context; - -// @public -export interface Span { - addEvent(name: string, attributesOrStartTime?: SpanAttributes | TimeInput, startTime?: TimeInput): this; - end(endTime?: TimeInput): void; - isRecording(): boolean; - recordException(exception: Exception, time?: TimeInput): void; - setAttribute(key: string, value: SpanAttributeValue): this; - setAttributes(attributes: SpanAttributes): this; - setStatus(status: SpanStatus): this; - spanContext(): SpanContext; - updateName(name: string): this; + tracingContext?: TracingContext; } // @public -export interface SpanAttributes { - [attributeKey: string]: SpanAttributeValue | undefined; -} - -// @public -export type SpanAttributeValue = string | number | boolean | Array | Array | Array; - -// @public -export interface SpanContext { - spanId: string; - traceFlags: number; - traceId: string; - traceState?: TraceState; -} +export type SpanStatus = { + status: "success"; +} | { + status: "error"; + error?: Error | string; +}; // @public -export enum SpanKind { - CLIENT = 2, - CONSUMER = 4, - INTERNAL = 0, - PRODUCER = 3, - SERVER = 1 +export interface TracingClient { + createRequestHeaders(tracingContext?: TracingContext): Record; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + startSpan(name: string, operationOptions?: Options, spanOptions?: TracingSpanOptions): { + span: TracingSpan; + updatedOptions: Options; + }; + withContext ReturnType>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType; + withSpan) => ReturnType>(name: string, operationOptions: Options, callback: Callback, spanOptions?: TracingSpanOptions): Promise>; } // @public -export interface SpanOptions { - attributes?: SpanAttributes; - kind?: SpanKind; - links?: Link[]; - startTime?: TimeInput; +export interface TracingClientOptions { + namespace: string; + packageName: string; + packageVersion?: string; } // @public -export interface SpanStatus { - code: SpanStatusCode; - message?: string; +export interface TracingContext { + deleteValue(key: symbol): TracingContext; + getValue(key: symbol): unknown; + setValue(key: symbol, value: unknown): TracingContext; } // @public -export enum SpanStatusCode { - ERROR = 2, - OK = 1, - UNSET = 0 +export interface TracingSpan { + end(): void; + isRecording(): boolean; + recordException(exception: Error | string): void; + setAttribute(name: string, value: unknown): void; + setStatus(status: SpanStatus): void; } // @public -export type TimeInput = HrTime | number | Date; +export type TracingSpanKind = "client" | "server" | "producer" | "consumer" | "internal"; // @public -export const enum TraceFlags { - NONE = 0, - SAMPLED = 1 +export interface TracingSpanLink { + attributes?: { + [key: string]: unknown; + }; + tracingContext: TracingContext; } // @public -export interface Tracer { - startSpan(name: string, options?: SpanOptions, context?: Context): Span; +export interface TracingSpanOptions { + spanAttributes?: { + [key: string]: unknown; + }; + spanKind?: TracingSpanKind; + spanLinks?: TracingSpanLink[]; } // @public -export interface TraceState { - get(key: string): string | undefined; - serialize(): string; - set(key: string, value: string): TraceState; - unset(key: string): TraceState; -} +export function useInstrumenter(instrumenter: Instrumenter): void; // (No @packageDocumentation comment for this package) diff --git a/sdk/core/core-tracing/src/createSpan.ts b/sdk/core/core-tracing/src/createSpan.ts deleted file mode 100644 index f7fb4f76954d..000000000000 --- a/sdk/core/core-tracing/src/createSpan.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - Context, - OperationTracingOptions, - Span, - SpanKind, - SpanOptions, - getTracer, - context as otContext, - setSpan, -} from "./interfaces"; -import { INVALID_SPAN_CONTEXT, trace } from "@opentelemetry/api"; - -/** - * Arguments for `createSpanFunction` that allow you to specify the - * prefix for each created span as well as the `az.namespace` attribute. - * - * @hidden - */ -export interface CreateSpanFunctionArgs { - /** - * Package name prefix. - * - * NOTE: if this is empty no prefix will be applied to created Span names. - */ - packagePrefix: string; - /** - * Service namespace - * - * NOTE: if this is empty no `az.namespace` attribute will be added to created Spans. - */ - namespace: string; -} - -/** - * @internal - * A set of known span attributes that will exist on a context - */ -export const knownSpanAttributes = { - AZ_NAMESPACE: { contextKey: Symbol.for("az.namespace"), spanAttributeName: "az.namespace" }, -}; - -/** - * Checks whether tracing is disabled by checking the `AZURE_TRACING_DISABLED` environment variable. - * - * @returns - `true` if tracing is disabled, `false` otherwise. - * - * @internal - */ -export function isTracingDisabled(): boolean { - if (typeof process === "undefined") { - // not supported in browser for now without polyfills - return false; - } - - const azureTracingDisabledValue = process.env.AZURE_TRACING_DISABLED?.toLowerCase(); - - if (azureTracingDisabledValue === "false" || azureTracingDisabledValue === "0") { - return false; - } - - return Boolean(azureTracingDisabledValue); -} - -/** - * Maintains backwards compatibility with the previous `OperationTracingOptions` in core-tracing preview.13 and earlier - * which passed `spanOptions` as part of `tracingOptions`. - */ -function disambiguateParameters( - operationOptions: T, - startSpanOptions?: SpanOptions -): [OperationTracingOptions, SpanOptions] { - const { tracingOptions } = operationOptions; - - // If startSpanOptions is provided, then we are using the new signature, - // otherwise try to pluck it out of the tracingOptions. - const spanOptions: SpanOptions = startSpanOptions || (tracingOptions as any)?.spanOptions || {}; - spanOptions.kind = spanOptions.kind || SpanKind.INTERNAL; - - return [tracingOptions || {}, spanOptions]; -} - -/** - * Creates a new span using the given parameters. - * - * @param spanName - The name of the span to created. - * @param spanOptions - Initialization options that can be used to customize the created span. - * @param tracingContext - The tracing context to use for the created span. - * - * @returns - A new span. - */ -function startSpan(spanName: string, spanOptions: SpanOptions, tracingContext: Context) { - if (isTracingDisabled()) { - return trace.wrapSpanContext(INVALID_SPAN_CONTEXT); - } - - const tracer = getTracer(); - return tracer.startSpan(spanName, spanOptions, tracingContext); -} - -/** - * Adds the `az.namespace` attribute on a span, the tracingContext, and the spanOptions - * - * @param span - The span to add the attribute to in place. - * @param tracingContext - The context bag to add the attribute to by creating a new context with the attribute. - * @param namespace - The value of the attribute. - * @param spanOptions - The spanOptions to add the attribute to (for backwards compatibility). - * - * @internal - * - * @returns The updated span options and context. - */ -function setNamespaceOnSpan( - span: Span, - tracingContext: Context, - namespace: string, - spanOptions: SpanOptions -) { - span.setAttribute(knownSpanAttributes.AZ_NAMESPACE.spanAttributeName, namespace); - const updatedContext = tracingContext.setValue( - knownSpanAttributes.AZ_NAMESPACE.contextKey, - namespace - ); - - // Here for backwards compatibility, but can be removed once we no longer use `spanOptions` (every client and core library depends on a version higher than preview.13) - const updatedSpanOptions = { - ...spanOptions, - attributes: { - ...spanOptions?.attributes, - [knownSpanAttributes.AZ_NAMESPACE.spanAttributeName]: namespace, - }, - }; - - return { updatedSpanOptions, updatedContext }; -} - -/** - * Creates a function that can be used to create spans using the global tracer. - * - * Usage: - * - * ```typescript - * // once - * const createSpan = createSpanFunction({ packagePrefix: "Azure.Data.AppConfiguration", namespace: "Microsoft.AppConfiguration" }); - * - * // in each operation - * const span = createSpan("deleteConfigurationSetting", operationOptions, startSpanOptions ); - * // code... - * span.end(); - * ``` - * - * @param args - allows configuration of the prefix for each span as well as the az.namespace field. - */ -export function createSpanFunction(args: CreateSpanFunctionArgs) { - /** - * Creates a span using the global tracer provider. - * - * @param operationName - The name of the operation to create a span for. - * @param operationOptions - The operation options containing the currently active tracing context when using manual span propagation. - * @param startSpanOptions - The options to use when creating the span, and will be passed to the tracer.startSpan method. - * - * @returns - A span from the global tracer provider, and an updatedOptions bag containing the new tracing context. - * - * Example usage: - * ```ts - * const { span, updatedOptions } = createSpan("deleteConfigurationSetting", operationOptions, startSpanOptions); - * ``` - */ - return function ( - operationName: string, - operationOptions?: T, - startSpanOptions?: SpanOptions - ): { span: Span; updatedOptions: T } { - const [tracingOptions, spanOptions] = disambiguateParameters( - operationOptions || ({} as T), - startSpanOptions - ); - - let tracingContext = tracingOptions?.tracingContext || otContext.active(); - - const spanName = args.packagePrefix ? `${args.packagePrefix}.${operationName}` : operationName; - const span = startSpan(spanName, spanOptions, tracingContext); - - let newSpanOptions = spanOptions; - if (args.namespace) { - const { updatedSpanOptions, updatedContext } = setNamespaceOnSpan( - span, - tracingContext, - args.namespace, - spanOptions - ); - - tracingContext = updatedContext; - newSpanOptions = updatedSpanOptions; - } - - const newTracingOptions = { - ...tracingOptions, - spanOptions: newSpanOptions, - tracingContext: setSpan(tracingContext, span), - }; - - const newOperationOptions = { - ...(operationOptions as T), - tracingOptions: newTracingOptions, - }; - - return { - span, - updatedOptions: newOperationOptions, - }; - }; -} diff --git a/sdk/core/core-tracing/src/index.ts b/sdk/core/core-tracing/src/index.ts index 9d76dba24cd8..082ca88bf334 100644 --- a/sdk/core/core-tracing/src/index.ts +++ b/sdk/core/core-tracing/src/index.ts @@ -1,43 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Tracers and wrappers -export { createSpanFunction, CreateSpanFunctionArgs } from "./createSpan"; - -// Shared interfaces export { - context, - Context, - ContextAPI, - Exception, - ExceptionWithCode, - ExceptionWithMessage, - ExceptionWithName, - getSpan, - getSpanContext, - getTracer, - HrTime, - isSpanContextValid, - Link, + Instrumenter, + InstrumenterSpanOptions, OperationTracingOptions, - setSpan, - setSpanContext, - Span, - SpanAttributes, - SpanAttributeValue, - SpanContext, - SpanKind, - SpanOptions, SpanStatus, - SpanStatusCode, - TimeInput, - TraceFlags, - Tracer, - TraceState, + TracingClient, + TracingClientOptions, + TracingContext, + TracingSpan, + TracingSpanKind, + TracingSpanLink, + TracingSpanOptions, } from "./interfaces"; - -// Utilities -export { - extractSpanContextFromTraceParentHeader, - getTraceParentHeader, -} from "./utils/traceParentHeader"; +export { useInstrumenter } from "./instrumenter"; +export { createTracingClient } from "./tracingClient"; diff --git a/sdk/core/core-tracing/src/instrumenter.ts b/sdk/core/core-tracing/src/instrumenter.ts new file mode 100644 index 000000000000..b2898e6a387f --- /dev/null +++ b/sdk/core/core-tracing/src/instrumenter.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Instrumenter, InstrumenterSpanOptions, TracingContext, TracingSpan } from "./interfaces"; +import { createTracingContext } from "./tracingContext"; + +export function createDefaultTracingSpan(): TracingSpan { + return { + end: () => { + // noop + }, + isRecording: () => false, + recordException: () => { + // noop + }, + setAttribute: () => { + // noop + }, + setStatus: () => { + // noop + }, + }; +} + +export function createDefaultInstrumenter(): Instrumenter { + return { + createRequestHeaders: (): Record => { + return {}; + }, + parseTraceparentHeader: (): TracingContext | undefined => { + return undefined; + }, + startSpan: ( + _name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } => { + return { + span: createDefaultTracingSpan(), + tracingContext: createTracingContext({ parentContext: spanOptions.tracingContext }), + }; + }, + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + _context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return callback(...callbackArgs); + }, + }; +} + +/** @internal */ +let instrumenterImplementation: Instrumenter | undefined; + +/** + * Extends the Azure SDK with support for a given instrumenter implementation. + * + * @param instrumenter - The instrumenter implementation to use. + */ +export function useInstrumenter(instrumenter: Instrumenter): void { + instrumenterImplementation = instrumenter; +} + +/** + * Gets the currently set instrumenter, a No-Op instrumenter by default. + * + * @returns The currently set instrumenter + */ +export function getInstrumenter(): Instrumenter { + if (!instrumenterImplementation) { + instrumenterImplementation = createDefaultInstrumenter(); + } + return instrumenterImplementation; +} diff --git a/sdk/core/core-tracing/src/interfaces.ts b/sdk/core/core-tracing/src/interfaces.ts index d5470dcb1416..b0e09fe3228e 100644 --- a/sdk/core/core-tracing/src/interfaces.ts +++ b/sdk/core/core-tracing/src/interfaces.ts @@ -1,500 +1,265 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { context as otContext, trace as otTrace } from "@opentelemetry/api"; - /** - * A Tracer. + * Represents a client that can integrate with the currently configured {@link Instrumenter}. + * + * Create an instance using {@link createTracingClient}. */ -export interface Tracer { +export interface TracingClient { /** - * Starts a new {@link Span}. Start the span without setting it on context. + * Wraps a callback in a tracing span, calls the callback, and closes the span. * - * This method does NOT modify the current Context. + * This is the primary interface for using Tracing and will handle error recording as well as setting the status on the span. * - * @param name - The name of the span - * @param options - SpanOptions used for span creation - * @param context - Context to use to extract parent - * @returns The newly created span - * @example - * const span = tracer.startSpan('op'); - * span.setAttribute('key', 'value'); - * span.end(); - */ - startSpan(name: string, options?: SpanOptions, context?: Context): Span; -} - -/** - * TraceState. - */ -export interface TraceState { + * Example: + * + * ```ts + * const myOperationResult = await tracingClient.withSpan("myClassName.myOperationName", options, (updatedOptions) => myOperation(updatedOptions)); + * ``` + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param operationOptions - The original options passed to the method. The callback will receive these options with the newly created {@link TracingContext}. + * @param callback - The callback to be invoked with the updated options and newly created {@link TracingSpan}. + */ + withSpan< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends ( + updatedOptions: Options, + span: Omit + ) => ReturnType + >( + name: string, + operationOptions: Options, + callback: Callback, + spanOptions?: TracingSpanOptions + ): Promise>; /** - * Create a new TraceState which inherits from this TraceState and has the - * given key set. - * The new entry will always be added in the front of the list of states. + * Starts a given span but does not set it as the active span. + * + * You must end the span using {@link TracingSpan.end}. * - * @param key - key of the TraceState entry. - * @param value - value of the TraceState entry. + * Most of the time you will want to use {@link withSpan} instead. + * + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param operationOptions - The original operation options. + * @param spanOptions - The options to use when creating the span. + * + * @returns A {@link TracingSpan} and the updated operation options. */ - set(key: string, value: string): TraceState; + startSpan( + name: string, + operationOptions?: Options, + spanOptions?: TracingSpanOptions + ): { span: TracingSpan; updatedOptions: Options }; /** - * Return a new TraceState which inherits from this TraceState but does not - * contain the given key. + * Wraps a callback with an active context and calls the callback. + * Depending on the implementation, this may set the globally available active context. * - * @param key - the key for the TraceState entry to be removed. + * Useful when you want to leave the boundaries of the SDK (make a request or callback to user code) and are unable to use the {@link withSpan} API. + * + * @param context - The {@link TracingContext} to use as the active context in the scope of the callback. + * @param callback - The callback to be invoked with the given context set as the globally active context. + * @param callbackArgs - The callback arguments. */ - unset(key: string): TraceState; + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType; + /** - * Returns the value to which the specified key is mapped, or `undefined` if - * this map contains no mapping for the key. + * Parses a traceparent header value into a {@link TracingSpanContext}. * - * @param key - with which the specified value is to be associated. - * @returns the value to which the specified key is mapped, or `undefined` if - * this map contains no mapping for the key. + * @param traceparentHeader - The traceparent header to parse. + * @returns An implementation-specific identifier for the span. */ - get(key: string): string | undefined; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + /** - * Serializes the TraceState to a `list` as defined below. The `list` is a - * series of `list-members` separated by commas `,`, and a list-member is a - * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs - * surrounding `list-members` are ignored. There can be a maximum of 32 - * `list-members` in a `list`. + * Creates a set of request headers to propagate tracing information to a backend. * - * @returns the serialized string. + * @param tracingContext - The context containing the span to propagate. + * @returns The set of headers to add to a request. */ - serialize(): string; + createRequestHeaders(tracingContext?: TracingContext): Record; } /** - * Represents high resolution time. + * Options that can be passed to {@link createTracingClient} */ -export declare type HrTime = [number, number]; +export interface TracingClientOptions { + /** The value of the az.namespace tracing attribute on newly created spans. */ + namespace: string; + /** The name of the package invoking this trace. */ + packageName: string; + /** An optional version of the package invoking this trace. */ + packageVersion?: string; +} -/** - * Used to represent a Time. - */ -export type TimeInput = HrTime | number | Date; +/** The kind of span. */ +export type TracingSpanKind = "client" | "server" | "producer" | "consumer" | "internal"; + +/** Options used to configure the newly created span. */ +export interface TracingSpanOptions { + /** The kind of span. Implementations should default this to "client". */ + spanKind?: TracingSpanKind; + /** A collection of {@link TracingSpanLink} to link to this span. */ + spanLinks?: TracingSpanLink[]; + /** Initial set of attributes to set on a span. */ + spanAttributes?: { [key: string]: unknown }; +} -/** - * The status for a span. - */ -export interface SpanStatus { - /** The status code of this message. */ - code: SpanStatusCode; - /** A developer-facing error message. */ - message?: string; +/** A pointer from the current {@link TracingSpan} to another span in the same or a different trace. */ +export interface TracingSpanLink { + /** The {@link TracingContext} containing the span context to link to. */ + tracingContext: TracingContext; + /** A set of attributes on the link. */ + attributes?: { [key: string]: unknown }; } /** - * The kind of span. + * Represents an implementation agnostic instrumenter. */ -export enum SpanKind { - /** Default value. Indicates that the span is used internally. */ - INTERNAL = 0, +export interface Instrumenter { /** - * Indicates that the span covers server-side handling of an RPC or other - * remote request. + * Creates a new {@link TracingSpan} with the given name and options and sets it on a new context. + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param spanOptions - The options to use when creating the span. + * + * @returns A {@link TracingSpan} that can be used to end the span, and the context this span has been set on. */ - SERVER = 1, + startSpan( + name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext }; /** - * Indicates that the span covers the client-side wrapper around an RPC or - * other remote request. + * Wraps a callback with an active context and calls the callback. + * Depending on the implementation, this may set the globally available active context. + * + * @param context - The {@link TracingContext} to use as the active context in the scope of the callback. + * @param callback - The callback to be invoked with the given context set as the globally active context. + * @param callbackArgs - The callback arguments. */ - CLIENT = 2, + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType; + /** - * Indicates that the span describes producer sending a message to a - * broker. Unlike client and server, there is no direct critical path latency - * relationship between producer and consumer spans. + * Provides an implementation-specific method to parse a {@link https://www.w3.org/TR/trace-context/#traceparent-header} + * into a {@link TracingSpanContext} which can be used to link non-parented spans together. */ - PRODUCER = 3, + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; /** - * Indicates that the span describes consumer receiving a message from a - * broker. Unlike client and server, there is no direct critical path latency - * relationship between producer and consumer spans. + * Provides an implementation-specific method to serialize a {@link TracingSpan} to a set of headers. + * @param tracingContext - The context containing the span to serialize. */ - CONSUMER = 4, -} - -/** - * An Exception for a Span. - */ -export declare type Exception = - | ExceptionWithCode - | ExceptionWithMessage - | ExceptionWithName - | string; - -/** - * An Exception with a code. - */ -export interface ExceptionWithCode { - /** The code. */ - code: string | number; - /** The name. */ - name?: string; - /** The message. */ - message?: string; - /** The stack. */ - stack?: string; -} - -/** - * An Exception with a message. - */ -export interface ExceptionWithMessage { - /** The code. */ - code?: string | number; - /** The message. */ - message: string; - /** The name. */ - name?: string; - /** The stack. */ - stack?: string; -} - -/** - * An Exception with a name. - */ -export interface ExceptionWithName { - /** The code. */ - code?: string | number; - /** The message. */ - message?: string; - /** The name. */ - name: string; - /** The stack. */ - stack?: string; -} - -/** - * Return the span if one exists - * - * @param context - context to get span from - */ -export function getSpan(context: Context): Span | undefined { - return otTrace.getSpan(context); -} - -/** - * Set the span on a context - * - * @param context - context to use as parent - * @param span - span to set active - */ -export function setSpan(context: Context, span: Span): Context { - return otTrace.setSpan(context, span); + createRequestHeaders(tracingContext?: TracingContext): Record; } /** - * Wrap span context in a NoopSpan and set as span in a new - * context - * - * @param context - context to set active span on - * @param spanContext - span context to be wrapped + * Options passed to {@link Instrumenter.startSpan} as a superset of {@link TracingSpanOptions}. */ -export function setSpanContext(context: Context, spanContext: SpanContext): Context { - return otTrace.setSpanContext(context, spanContext); +export interface InstrumenterSpanOptions extends TracingSpanOptions { + /** The name of the package invoking this trace. */ + packageName: string; + /** The version of the package invoking this trace. */ + packageVersion?: string; + /** The current tracing context. Defaults to an implementation-specific "active" context. */ + tracingContext?: TracingContext; } /** - * Get the span context of the span if it exists. + * Represents the statuses that can be passed to {@link TracingSpan.setStatus}. * - * @param context - context to get values from - */ -export function getSpanContext(context: Context): SpanContext | undefined { - return otTrace.getSpanContext(context); -} - -/** - * Singleton object which represents the entry point to the OpenTelemetry Context API + * By default, all spans will be created with status "unset". */ -export interface ContextAPI { - /** - * Get the currently active context - */ - active(): Context; -} - -/** - * Returns true of the given {@link SpanContext} is valid. - * A valid {@link SpanContext} is one which has a valid trace ID and span ID as per the spec. - * - * @param context - the {@link SpanContext} to validate. - * - * @returns true if the {@link SpanContext} is valid, false otherwise. - */ -export function isSpanContextValid(context: SpanContext): boolean { - return otTrace.isSpanContextValid(context); -} - -/** - * Retrieves a tracer from the global tracer provider. - */ -export function getTracer(): Tracer; -/** - * Retrieves a tracer from the global tracer provider. - */ -export function getTracer(name: string, version?: string): Tracer; -export function getTracer(name?: string, version?: string): Tracer { - return otTrace.getTracer(name || "azure/core-tracing", version); -} - -/** Entrypoint for context API */ -export const context: ContextAPI = otContext; - -/** SpanStatusCode */ -export enum SpanStatusCode { - /** - * The default status. - */ - UNSET = 0, - /** - * The operation has been validated by an Application developer or - * Operator to have completed successfully. - */ - OK = 1, - /** - * The operation contains an error. - */ - ERROR = 2, -} +export type SpanStatus = + | { + status: "success"; + } + | { + status: "error"; + error?: Error | string; + }; /** - * An interface that represents a span. A span represents a single operation - * within a trace. Examples of span might include remote procedure calls or a - * in-process function calls to sub-components. A Trace has a single, top-level - * "root" Span that in turn may have zero or more child Spans, which in turn - * may have children. - * - * Spans are created by the {@link Tracer.startSpan} method. + * Represents an implementation agnostic tracing span. */ -export interface Span { - /** - * Returns the {@link SpanContext} object associated with this Span. - * - * Get an immutable, serializable identifier for this span that can be used - * to create new child spans. Returned SpanContext is usable even after the - * span ends. - * - * @returns the SpanContext object associated with this Span. - */ - spanContext(): SpanContext; - /** - * Sets an attribute to the span. - * - * Sets a single Attribute with the key and value passed as arguments. - * - * @param key - the key for this attribute. - * @param value - the value for this attribute. Setting a value null or - * undefined is invalid and will result in undefined behavior. - */ - setAttribute(key: string, value: SpanAttributeValue): this; - /** - * Sets attributes to the span. - * - * @param attributes - the attributes that will be added. - * null or undefined attribute values - * are invalid and will result in undefined behavior. - */ - setAttributes(attributes: SpanAttributes): this; - /** - * Adds an event to the Span. - * - * @param name - the name of the event. - * @param attributesOrStartTime - the attributes that will be added; these are - * associated with this event. Can be also a start time - * if type is TimeInput and 3rd param is undefined - * @param startTime - start time of the event. - */ - addEvent( - name: string, - attributesOrStartTime?: SpanAttributes | TimeInput, - startTime?: TimeInput - ): this; +export interface TracingSpan { /** - * Sets a status to the span. If used, this will override the default Span - * status. Default is {@link SpanStatusCode.UNSET}. SetStatus overrides the value - * of previous calls to SetStatus on the Span. + * Sets the status of the span. When an error is provided, it will be recorded on the span as well. * - * @param status - the SpanStatus to set. + * @param status - The {@link SpanStatus} to set on the span. */ - setStatus(status: SpanStatus): this; - /** - * Marks the end of Span execution. - * - * Call to End of a Span MUST not have any effects on child spans. Those may - * still be running and can be ended later. - * - * Do not return `this`. The Span generally should not be used after it - * is ended so chaining is not desired in this context. - * - * @param endTime - the time to set as Span's end time. If not provided, - * use the current time as the span's end time. - */ - end(endTime?: TimeInput): void; + setStatus(status: SpanStatus): void; + /** - * Returns the flag whether this span will be recorded. + * Sets a given attribute on a span. * - * @returns true if this Span is active and recording information like events - * with the `AddEvent` operation and attributes using `setAttributes`. + * @param name - The attribute's name. + * @param value - The attribute's value to set. May be any non-nullish value. */ - isRecording(): boolean; + setAttribute(name: string, value: unknown): void; /** - * Sets exception as a span event - * @param exception - the exception the only accepted values are string or Error - * @param time - the time to set as Span's event time. If not provided, - * use the current time. + * Ends the span. */ - recordException(exception: Exception, time?: TimeInput): void; + end(): void; /** - * Updates the Span name. + * Records an exception on a {@link TracingSpan} without modifying its status. * - * This will override the name provided via {@link Tracer.startSpan}. + * When recording an unhandled exception that should fail the span, please use {@link TracingSpan.setStatus} instead. * - * Upon this update, any sampling behavior based on Span name will depend on - * the implementation. + * @param exception - The exception to record on the span. * - * @param name - the Span name. */ - updateName(name: string): this; -} - -/** - * Shorthand enum for common traceFlags values inside SpanContext - */ -export const enum TraceFlags { - /** No flag set. */ - NONE = 0x0, - /** Caller is collecting trace information. */ - SAMPLED = 0x1, -} + recordException(exception: Error | string): void; -/** - * A light interface that tries to be structurally compatible with OpenTelemetry - */ -export interface SpanContext { /** - * UUID of a trace. - */ - traceId: string; - /** - * UUID of a Span. - */ - spanId: string; - /** - * https://www.w3.org/TR/trace-context/#trace-flags - */ - traceFlags: number; - /** - * Tracing-system-specific info to propagate. - * - * The tracestate field value is a `list` as defined below. The `list` is a - * series of `list-members` separated by commas `,`, and a list-member is a - * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs - * surrounding `list-members` are ignored. There can be a maximum of 32 - * `list-members` in a `list`. - * More Info: https://www.w3.org/TR/trace-context/#tracestate-field + * Returns true if this {@link TracingSpan} is recording information. * - * Examples: - * Single tracing system (generic format): - * tracestate: rojo=00f067aa0ba902b7 - * Multiple tracing systems (with different formatting): - * tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + * Depending on the span implementation, this may return false if the span is not being sampled. */ - traceState?: TraceState; -} - -/** - * Used to specify a span that is linked to another. - */ -export interface Link { - /** The {@link SpanContext} of a linked span. */ - context: SpanContext; - - /** A set of {@link SpanAttributes} on the link. */ - attributes?: SpanAttributes; -} - -/** - * Attributes for a Span. - */ -export interface SpanAttributes { - /** - * Attributes for a Span. - */ - [attributeKey: string]: SpanAttributeValue | undefined; + isRecording(): boolean; } -/** - * Attribute values may be any non-nullish primitive value except an object. - * - * null or undefined attribute values are invalid and will result in undefined behavior. - */ -export declare type SpanAttributeValue = - | string - | number - | boolean - | Array - | Array - | Array; -/** - * An interface that enables manual propagation of Spans - */ -export interface SpanOptions { +/** An immutable context bag of tracing values for the current operation. */ +export interface TracingContext { /** - * Attributes to set on the Span + * Sets a given object on a context. + * @param key - The key of the given context value. + * @param value - The value to set on the context. + * + * @returns - A new context with the given value set. */ - attributes?: SpanAttributes; - - /** {@link Link}s span to other spans */ - links?: Link[]; - + setValue(key: symbol, value: unknown): TracingContext; /** - * The type of Span. Default to SpanKind.INTERNAL + * Gets an object from the context if it exists. + * @param key - The key of the given context value. + * + * @returns - The value of the given context value if it exists, otherwise `undefined`. */ - kind?: SpanKind; - + getValue(key: symbol): unknown; /** - * A manually specified start time for the created `Span` object. + * Deletes an object from the context if it exists. + * @param key - The key of the given context value to delete. */ - startTime?: TimeInput; + deleteValue(key: symbol): TracingContext; } /** * Tracing options to set on an operation. */ export interface OperationTracingOptions { - /** - * OpenTelemetry context to use for created Spans. - */ - tracingContext?: Context; -} - -/** - * OpenTelemetry compatible interface for Context - */ -export interface Context { - /** - * Get a value from the context. - * - * @param key - key which identifies a context value - */ - getValue(key: symbol): unknown; - /** - * Create a new context which inherits from this context and has - * the given key set to the given value. - * - * @param key - context key for which to set the value - * @param value - value to set for the given key - */ - setValue(key: symbol, value: unknown): Context; - /** - * Return a new context which inherits from this context but does - * not contain a value for the given key. - * - * @param key - context key for which to clear a value - */ - deleteValue(key: symbol): Context; + /** The context to use for created Tracing Spans. */ + tracingContext?: TracingContext; } diff --git a/sdk/core/core-tracing/src/tracingClient.ts b/sdk/core/core-tracing/src/tracingClient.ts new file mode 100644 index 000000000000..fa39b03d9214 --- /dev/null +++ b/sdk/core/core-tracing/src/tracingClient.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + OperationTracingOptions, + TracingClient, + TracingClientOptions, + TracingContext, + TracingSpan, + TracingSpanOptions, +} from "./interfaces"; +import { getInstrumenter } from "./instrumenter"; +import { knownContextKeys } from "./tracingContext"; + +/** + * Creates a new tracing client. + * + * @param options - Options used to configure the tracing client. + * @returns - An instance of {@link TracingClient}. + */ +export function createTracingClient(options: TracingClientOptions): TracingClient { + const { namespace, packageName, packageVersion } = options; + + function startSpan( + name: string, + operationOptions?: Options, + spanOptions?: TracingSpanOptions + ): { + span: TracingSpan; + updatedOptions: Options; + } { + const startSpanResult = getInstrumenter().startSpan(name, { + ...spanOptions, + packageName: packageName, + packageVersion: packageVersion, + tracingContext: operationOptions?.tracingOptions?.tracingContext, + }); + let tracingContext = startSpanResult.tracingContext; + const span = startSpanResult.span; + if (!tracingContext.getValue(knownContextKeys.namespace)) { + tracingContext = tracingContext.setValue(knownContextKeys.namespace, namespace); + } + span.setAttribute("az.namespace", tracingContext.getValue(knownContextKeys.namespace)); + const updatedOptions = { + ...operationOptions, + tracingOptions: { + tracingContext: tracingContext, + }, + } as Options; + return { + span, + updatedOptions, + }; + } + + async function withSpan< + Options extends { tracingOptions?: { tracingContext?: TracingContext } }, + Callback extends ( + updatedOptions: Options, + span: Omit + ) => ReturnType + >( + name: string, + operationOptions: Options, + callback: Callback, + spanOptions?: TracingSpanOptions + ): Promise> { + const { span, updatedOptions } = startSpan(name, operationOptions, spanOptions); + try { + const result = await withContext(updatedOptions.tracingOptions!.tracingContext!, () => + Promise.resolve(callback(updatedOptions, span)) + ); + span.setStatus({ status: "success" }); + return result; + } catch (err) { + span.setStatus({ status: "error", error: err }); + throw err; + } finally { + span.end(); + } + } + + function withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return getInstrumenter().withContext(context, callback, ...callbackArgs); + } + + /** + * Parses a traceparent header value into a span identifier. + * + * @param traceparentHeader - The traceparent header to parse. + * @returns An implementation-specific identifier for the span. + */ + function parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined { + return getInstrumenter().parseTraceparentHeader(traceparentHeader); + } + + /** + * Creates a set of request headers to propagate tracing information to a backend. + * + * @param tracingContext - The context containing the span to serialize. + * @returns The set of headers to add to a request. + */ + function createRequestHeaders(tracingContext?: TracingContext): Record { + return getInstrumenter().createRequestHeaders(tracingContext); + } + + return { + startSpan, + withSpan, + withContext, + parseTraceparentHeader, + createRequestHeaders, + }; +} diff --git a/sdk/core/core-tracing/src/tracingContext.ts b/sdk/core/core-tracing/src/tracingContext.ts new file mode 100644 index 000000000000..7c0d6369561e --- /dev/null +++ b/sdk/core/core-tracing/src/tracingContext.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracingClient, TracingContext, TracingSpan } from "./interfaces"; + +/** @internal */ +export const knownContextKeys = { + span: Symbol.for("@azure/core-tracing span"), + namespace: Symbol.for("@azure/core-tracing namespace"), + client: Symbol.for("@azure/core-tracing client"), + parentContext: Symbol.for("@azure/core-tracing parent context"), +}; + +/** + * Creates a new {@link TracingContext} with the given options. + * @param options - A set of known keys that may be set on the context. + * @returns A new {@link TracingContext} with the given options. + * + * @internal + */ +export function createTracingContext(options: CreateTracingContextOptions = {}): TracingContext { + let context: TracingContext = new TracingContextImpl(options.parentContext); + if (options.span) { + context = context.setValue(knownContextKeys.span, options.span); + } + if (options.client) { + context = context.setValue(knownContextKeys.client, options.client); + } + if (options.namespace) { + context = context.setValue(knownContextKeys.namespace, options.namespace); + } + return context; +} + +/** @internal */ +export class TracingContextImpl implements TracingContext { + private _contextMap: Map; + constructor(initialContext?: TracingContext) { + this._contextMap = + initialContext instanceof TracingContextImpl + ? new Map(initialContext._contextMap) + : new Map(); + } + + setValue(key: symbol, value: unknown): TracingContext { + const newContext = new TracingContextImpl(this); + newContext._contextMap.set(key, value); + return newContext; + } + + getValue(key: symbol): unknown { + return this._contextMap.get(key); + } + + deleteValue(key: symbol): TracingContext { + const newContext = new TracingContextImpl(this); + newContext._contextMap.delete(key); + return newContext; + } +} + +/** + * Represents a set of items that can be set when creating a new {@link TracingContext}. + */ +export interface CreateTracingContextOptions { + /** The {@link parentContext} - the newly created context will contain all the values of the parent context unless overriden. */ + parentContext?: TracingContext; + /** An initial span to set on the context. */ + span?: TracingSpan; + /** The tracing client used to create this context. */ + client?: TracingClient; + /** The namespace to set on any child spans. */ + namespace?: string; +} diff --git a/sdk/core/core-tracing/src/utils/traceParentHeader.ts b/sdk/core/core-tracing/src/utils/traceParentHeader.ts deleted file mode 100644 index 67b394e4d157..000000000000 --- a/sdk/core/core-tracing/src/utils/traceParentHeader.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SpanContext, TraceFlags } from "../interfaces"; - -const VERSION = "00"; - -/** - * Generates a `SpanContext` given a `traceparent` header value. - * @param traceParent - Serialized span context data as a `traceparent` header value. - * @returns The `SpanContext` generated from the `traceparent` value. - */ -export function extractSpanContextFromTraceParentHeader( - traceParentHeader: string -): SpanContext | undefined { - const parts = traceParentHeader.split("-"); - - if (parts.length !== 4) { - return; - } - - const [version, traceId, spanId, traceOptions] = parts; - - if (version !== VERSION) { - return; - } - - const traceFlags = parseInt(traceOptions, 16); - - const spanContext: SpanContext = { - spanId, - traceId, - traceFlags, - }; - - return spanContext; -} - -/** - * Generates a `traceparent` value given a span context. - * @param spanContext - Contains context for a specific span. - * @returns The `spanContext` represented as a `traceparent` value. - */ -export function getTraceParentHeader(spanContext: SpanContext): string | undefined { - const missingFields: string[] = []; - if (!spanContext.traceId) { - missingFields.push("traceId"); - } - if (!spanContext.spanId) { - missingFields.push("spanId"); - } - - if (missingFields.length) { - return; - } - - const flags = spanContext.traceFlags || TraceFlags.NONE; - const hexFlags = flags.toString(16); - const traceFlags = hexFlags.length === 1 ? `0${hexFlags}` : hexFlags; - - // https://www.w3.org/TR/trace-context/#traceparent-header-field-values - return `${VERSION}-${spanContext.traceId}-${spanContext.spanId}-${traceFlags}`; -} diff --git a/sdk/core/core-tracing/test/createSpan.spec.ts b/sdk/core/core-tracing/test/createSpan.spec.ts deleted file mode 100644 index a9843d2a92d1..000000000000 --- a/sdk/core/core-tracing/test/createSpan.spec.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - Context, - SpanKind, - getSpanContext, - context as otContext, - setSpan, -} from "../src/interfaces"; -import { createSpanFunction, isTracingDisabled, knownSpanAttributes } from "../src/createSpan"; -import { OperationTracingOptions } from "../src/interfaces"; -import { TestSpan } from "./util/testSpan"; -import { TestTracerProvider } from "./util/testTracerProvider"; -import { assert } from "chai"; - -describe("createSpan", () => { - let createSpan: ReturnType; - let tracerProvider: TestTracerProvider; - - beforeEach(() => { - tracerProvider = new TestTracerProvider(); - tracerProvider.register(); - createSpan = createSpanFunction({ namespace: "Microsoft.Test", packagePrefix: "Azure.Test" }); - }); - - afterEach(() => { - tracerProvider.disable(); - }); - - it("is backwards compatible at runtime with versions prior to preview.13", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("test"); - const someContext = setSpan(otContext.active(), testSpan); - - // Ensure we are backwards compatible with { tracingOptions: { spanOptions } } shape which was - // used prior to preview.13 for setting span options. - const options = { - tracingOptions: { - tracingContext: someContext, - spanOptions: { - kind: SpanKind.CLIENT, - attributes: { - foo: "bar", - }, - }, - }, - }; - - const expectedSpanOptions = { - kind: SpanKind.CLIENT, - attributes: { - foo: "bar", - "az.namespace": "Microsoft.Test", - }, - }; - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>( - createSpan("testMethod", options) - ); - assert.deepEqual(updatedOptions.tracingOptions.spanOptions, expectedSpanOptions); - assert.equal(span.kind, SpanKind.CLIENT); - assert.equal(span.attributes.foo, "bar"); - - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue( - knownSpanAttributes.AZ_NAMESPACE.contextKey - ), - "Microsoft.Test" - ); - }); - - it("returns a created span with the right metadata", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan); - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>createSpan( - "testMethod", - { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingOptions: { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingContext: someContext, - }, - }, - { kind: SpanKind.SERVER } - ); - assert.strictEqual(span.name, "Azure.Test.testMethod"); - assert.equal(span.attributes["az.namespace"], "Microsoft.Test"); - - assert.equal(updatedOptions.someOtherField, "someOtherFieldValue"); - assert.equal(updatedOptions.tracingOptions.someOtherField, "someOtherFieldValue"); - - assert.equal(span.kind, SpanKind.SERVER); - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue(Symbol.for("az.namespace")), - "Microsoft.Test" - ); - }); - - it("preserves existing attributes", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan).setValue( - Symbol.for("someOtherKey"), - "someOtherValue" - ); - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>( - createSpan("testMethod", { - someTopLevelField: "someTopLevelFieldValue", - tracingOptions: { - someOtherTracingField: "someOtherTracingValue", - tracingContext: someContext, - }, - }) - ); - assert.strictEqual(span.name, "Azure.Test.testMethod"); - assert.equal(span.attributes["az.namespace"], "Microsoft.Test"); - - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue(Symbol.for("someOtherKey")), - "someOtherValue" - ); - assert.equal(updatedOptions.someTopLevelField, "someTopLevelFieldValue"); - assert.equal(updatedOptions.tracingOptions.someOtherTracingField, "someOtherTracingValue"); - }); - - it("namespace and packagePrefix can be empty (and thus ignored)", () => { - const cf = createSpanFunction({ - namespace: "", - packagePrefix: "", - }); - - const { span, updatedOptions } = cf("myVerbatimOperationName", {} as any, { - attributes: { - testAttribute: "testValue", - }, - }); - - assert.equal( - (span as TestSpan).name, - "myVerbatimOperationName", - "Expected name to not change because there is no packagePrefix." - ); - assert.notExists( - (span as TestSpan).attributes["az.namespace"], - "Expected az.namespace not to be set because there is no namespace" - ); - - assert.notExists( - updatedOptions.tracingOptions.tracingContext?.getValue(Symbol.for("az.namespace")) - ); - }); - - it("createSpans, testing parent/child relationship", () => { - const createSpanFn = createSpanFunction({ - namespace: "Microsoft.Test", - packagePrefix: "Azure.Test", - }); - - let parentContext: Context; - - // create the parent span and do some basic checks. - { - const op: { tracingOptions: OperationTracingOptions } = { - tracingOptions: {}, - }; - - const { span, updatedOptions } = createSpanFn("parent", op); - assert.ok(span); - - parentContext = updatedOptions.tracingOptions!.tracingContext!; - - assert.ok(parentContext); - assert.notDeepEqual(parentContext, otContext.active(), "new child context should be created"); - assert.equal( - getSpanContext(parentContext!)?.spanId, - span.spanContext().spanId, - "context returned in the updated options should point to our newly created span" - ); - } - - const { span: childSpan, updatedOptions } = createSpanFn("child", { - tracingOptions: { - tracingContext: parentContext, - }, - }); - assert.ok(childSpan); - - assert.ok(updatedOptions.tracingOptions.tracingContext); - assert.equal( - getSpanContext(updatedOptions.tracingOptions.tracingContext!)?.spanId, - childSpan.spanContext().spanId - ); - }); - - it("is robust when no options are passed in", () => { - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>createSpan("foo"); - assert.exists(span); - assert.exists(updatedOptions); - assert.exists(updatedOptions.tracingOptions.spanOptions); - assert.exists(updatedOptions.tracingOptions.tracingContext); - }); - - it("returns a no-op tracer if AZURE_TRACING_DISABLED is set", function (this: Mocha.Context) { - if (typeof process === "undefined") { - this.skip(); - } - process.env.AZURE_TRACING_DISABLED = "true"; - - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan); - - const { span } = <{ span: TestSpan; updatedOptions: any }>createSpan("testMethod", { - tracingOptions: { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingContext: someContext, - spanOptions: { - kind: SpanKind.SERVER, - }, - } as OperationTracingOptions as any, - }); - assert.isFalse(span.isRecording()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - describe("IsTracingDisabled", () => { - beforeEach(function (this: Mocha.Context) { - if (typeof process === "undefined") { - this.skip(); - } - }); - it("is false when env var is blank or missing", () => { - process.env.AZURE_TRACING_DISABLED = ""; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - assert.isFalse(isTracingDisabled()); - }); - - it("is false when env var is 'false'", () => { - process.env.AZURE_TRACING_DISABLED = "false"; - assert.isFalse(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "False"; - assert.isFalse(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "FALSE"; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - it("is false when env var is 0", () => { - process.env.AZURE_TRACING_DISABLED = "0"; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - it("is true otherwise", () => { - process.env.AZURE_TRACING_DISABLED = "true"; - assert.isTrue(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "1"; - assert.isTrue(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - }); -}); diff --git a/sdk/core/core-tracing/test/instrumenter.spec.ts b/sdk/core/core-tracing/test/instrumenter.spec.ts new file mode 100644 index 000000000000..43ca92d749a1 --- /dev/null +++ b/sdk/core/core-tracing/test/instrumenter.spec.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { Instrumenter, TracingSpan } from "../src/interfaces"; +import { + createDefaultInstrumenter, + createDefaultTracingSpan, + getInstrumenter, + useInstrumenter, +} from "../src/instrumenter"; +import { createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("Instrumenter", () => { + describe("NoOpInstrumenter", () => { + let instrumenter: Instrumenter; + const name = "test-operation"; + + beforeEach(() => { + instrumenter = createDefaultInstrumenter(); + }); + + describe("#startSpan", () => { + const packageName = "test-package"; + + it("returns a new context", () => { + const { tracingContext } = instrumenter.startSpan(name, { packageName }); + assert.exists(tracingContext); + }); + + it("returns context with all existing properties", () => { + const [key, value] = [Symbol.for("key"), "value"]; + const context = createTracingContext().setValue(key, value); + + const { tracingContext } = instrumenter.startSpan(name, { + tracingContext: context, + packageName, + }); + assert.strictEqual(tracingContext.getValue(key), value); + }); + }); + + describe("#withContext", () => { + it("applies the callback", () => { + const expectedText = "expected"; + const result = instrumenter.withContext(createTracingContext(), () => expectedText); + assert.equal(result, expectedText); + }); + }); + + describe("#parseTraceparentHeader", () => { + it("returns undefined", () => { + assert.isUndefined(instrumenter.parseTraceparentHeader("")); + }); + }); + + describe("#createRequestHeaders", () => { + it("returns an empty object", () => { + assert.isEmpty(instrumenter.createRequestHeaders(createTracingContext())); + assert.isEmpty( + instrumenter.createRequestHeaders( + createTracingContext().setValue(knownContextKeys.span, createDefaultTracingSpan()) + ) + ); + }); + }); + }); + + describe("NoOpSpan", () => { + it("supports all TracingSpan methods", () => { + const span: TracingSpan = createDefaultTracingSpan(); + span.setStatus({ status: "success" }); + span.setAttribute("foo", "bar"); + span.recordException(new Error("test")); + span.end(); + assert.isFalse(span.isRecording()); + }); + }); + + describe("useInstrumenter", () => { + it("allows setting and getting a global instrumenter", () => { + const instrumenter = getInstrumenter(); + assert.exists(instrumenter); + + const newInstrumenter = createDefaultInstrumenter(); + useInstrumenter(newInstrumenter); + assert.strictEqual(getInstrumenter(), newInstrumenter); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/interfaces.spec.ts b/sdk/core/core-tracing/test/interfaces.spec.ts index 1284c397fdd9..5e8318326f7b 100644 --- a/sdk/core/core-tracing/test/interfaces.spec.ts +++ b/sdk/core/core-tracing/test/interfaces.spec.ts @@ -1,86 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as coreAuth from "@azure/core-auth"; -import * as coreTracing from "../src/interfaces"; -import * as openTelemetry from "@opentelemetry/api"; -import { TestTracer } from "./util/testTracer"; import { assert } from "chai"; -import { getTracer } from "../src/interfaces"; - -type coreAuthTracingOptions = Required["tracingOptions"]; - -describe("interface compatibility", () => { - it("SpanContext is assignable", () => { - const context: coreTracing.SpanContext = { - spanId: "", - traceId: "", - traceFlags: coreTracing.TraceFlags.NONE, - }; - - const OTContext: openTelemetry.SpanContext = context; - const context2: coreTracing.SpanContext = OTContext; - - assert.ok(context2); - }); - - it("SpanOptions can be passed to OT", () => { - const spanOptions: coreTracing.SpanOptions = { - attributes: { - hello: "world", - }, - kind: coreTracing.SpanKind.INTERNAL, - links: [ - { - context: { - traceFlags: coreTracing.TraceFlags.NONE, - spanId: "", - traceId: "", - }, - }, - ], - }; - - const oTSpanOptions: openTelemetry.SpanOptions = spanOptions; - assert.ok(oTSpanOptions); - }); - - it("core-auth", () => { - const coreTracingOptions: Required = { - tracingContext: coreTracing.context.active(), - }; - - const t: Required< - Omit< - coreAuthTracingOptions, - keyof Required | "spanOptions" - > - > = {}; - assert.ok(t, "core-tracing and core-auth should have the same properties"); - - const t2: Required< - Omit< - coreTracing.OperationTracingOptions, - keyof Required | "spanOptions" - > - > = {}; - assert.ok(t2, "core-tracing and core-auth should have the same properties"); - - const authTracingOptions: coreAuth.GetTokenOptions["tracingOptions"] = coreTracingOptions; - assert.ok(authTracingOptions); - }); - - describe("getTracer", () => { - it("returns a tracer with a given name and version", () => { - const tracer = getTracer("test", "1.0.0") as TestTracer; - assert.equal(tracer.name, "test"); - assert.equal(tracer.version, "1.0.0"); - }); +import { createTracingContext } from "../src/tracingContext"; +import * as coreTracing from "../src"; +import * as coreAuth from "@azure/core-auth"; - it("returns a tracer with a default name no version if not provided", () => { - const tracer = getTracer() as TestTracer; - assert.isNotEmpty(tracer.name); - assert.isUndefined(tracer.version); +describe("Interface compatibility", () => { + describe("OperationTracingOptions", () => { + it("is compatible with core-auth", () => { + const tracingOptions: coreTracing.OperationTracingOptions = { + tracingContext: createTracingContext({}), + }; + const authOptions: coreAuth.GetTokenOptions = { + tracingOptions, + }; + assert.ok(authOptions.tracingOptions); }); }); }); diff --git a/sdk/core/core-tracing/test/traceParentHeader.spec.ts b/sdk/core/core-tracing/test/traceParentHeader.spec.ts deleted file mode 100644 index 2a1ccd26d213..000000000000 --- a/sdk/core/core-tracing/test/traceParentHeader.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SpanContext, TraceFlags } from "@opentelemetry/api"; -import { extractSpanContextFromTraceParentHeader, getTraceParentHeader } from "../src"; -import { assert } from "chai"; - -describe("traceParentHeader", () => { - describe("#extractSpanContextFromTraceParentHeader", () => { - it("should extract a SpanContext from a properly formatted traceparent", () => { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; - const traceParentHeader = `00-${traceId}-${spanId}-${flags}`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - if (!spanContext) { - assert.fail("Extracted spanContext should be defined."); - return; - } - assert.equal(spanContext.traceId, traceId, "Extracted traceId does not match expectation."); - assert.equal(spanContext.spanId, spanId, "Extracted spanId does not match expectation."); - assert.equal( - spanContext.traceFlags, - TraceFlags.NONE, - "Extracted traceFlags do not match expectations." - ); - }); - - describe("should return undefined", () => { - it("when traceparent contains an unknown version", () => { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; - const traceParentHeader = `99-${traceId}-${spanId}-${flags}`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - - assert.strictEqual( - spanContext, - undefined, - "Invalid traceparent version should return undefined spanContext." - ); - }); - - it("when traceparent is malformed", () => { - const traceParentHeader = `123abc`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - - assert.strictEqual( - spanContext, - undefined, - "Malformed traceparent should return undefined spanContext." - ); - }); - }); - }); - - describe("#getTraceParentHeader", () => { - it("should return a traceparent header from a SpanContext", () => { - const spanContext: SpanContext = { - spanId: "2222222222222222", - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - `00-${spanContext.traceId}-${spanContext.spanId}-01`, - "TraceParentHeader does not match expectation." - ); - }); - - it("should set the traceFlag to UNSAMPLED if not provided in SpanContext", () => { - const spanContext: SpanContext = { - spanId: "2222222222222222", - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.NONE, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - `00-${spanContext.traceId}-${spanContext.spanId}-00`, - "TraceParentHeader does not match expectation." - ); - }); - - describe("should return undefined", () => { - it("when traceId is not defined", () => { - const spanContext: any = { - spanId: "2222222222222222", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - undefined, - "Missing traceId should return undefined spanContext." - ); - }); - - it("when spanId is not defined", () => { - const spanContext: any = { - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - undefined, - "Missing spanId should return undefined spanContext." - ); - }); - }); - }); -}); diff --git a/sdk/core/core-tracing/test/tracingClient.spec.ts b/sdk/core/core-tracing/test/tracingClient.spec.ts new file mode 100644 index 000000000000..1cb13421fbac --- /dev/null +++ b/sdk/core/core-tracing/test/tracingClient.spec.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import sinon from "sinon"; +import { Instrumenter, TracingClient, TracingContext, TracingSpan } from "../src/interfaces"; +import { + createDefaultInstrumenter, + createDefaultTracingSpan, + useInstrumenter, +} from "../src/instrumenter"; +import { createTracingClient } from "../src/tracingClient"; +import { createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("TracingClient", () => { + let instrumenter: Instrumenter; + let span: TracingSpan; + let context: TracingContext; + let client: TracingClient; + const expectedNamespace = "Microsoft.Test"; + + beforeEach(() => { + instrumenter = createDefaultInstrumenter(); + span = createDefaultTracingSpan(); + context = createTracingContext(); + + useInstrumenter(instrumenter); + client = createTracingClient({ + namespace: expectedNamespace, + packageName: "test-package", + packageVersion: "1.0.0", + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("#startSpan", () => { + it("sets namespace on span", () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setAttributeSpy = sinon.spy(span, "setAttribute"); + client.startSpan("test", {}); + assert.isTrue( + setAttributeSpy.calledWith("az.namespace", expectedNamespace), + `expected span.setAttribute("az.namespace", "${expectedNamespace}") to have been called` + ); + }); + + it("passes package information to instrumenter", () => { + const instrumenterStartSpanSpy = sinon.spy(instrumenter, "startSpan"); + client.startSpan("test", {}); + assert.isTrue(instrumenterStartSpanSpy.called); + const args = instrumenterStartSpanSpy.getCall(0).args; + + assert.equal(args[0], "test"); + assert.equal(args[1]?.packageName, "test-package"); + assert.equal(args[1]?.packageVersion, "1.0.0"); + }); + + it("sets namespace on context", () => { + const { updatedOptions } = client.startSpan("test"); + assert.equal( + updatedOptions.tracingOptions?.tracingContext?.getValue(knownContextKeys.namespace), + expectedNamespace + ); + }); + + it("does not override existing namespace on context", () => { + context = createTracingContext().setValue(knownContextKeys.namespace, "Existing.Namespace"); + const { updatedOptions } = client.startSpan("test", { + tracingOptions: { tracingContext: context }, + }); + assert.equal( + updatedOptions.tracingOptions?.tracingContext?.getValue(knownContextKeys.namespace), + "Existing.Namespace" + ); + }); + + it("Returns tracingContext in updatedOptions", () => { + let { updatedOptions } = client.startSpan("test"); + assert.exists(updatedOptions.tracingOptions?.tracingContext); + updatedOptions = client.startSpan("test", updatedOptions).updatedOptions; + assert.exists(updatedOptions.tracingOptions?.tracingContext); + }); + }); + + describe("#withSpan", () => { + const spanName = "test-span"; + + it("sets namespace on span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setAttributeSpy = sinon.spy(span, "setAttribute"); + await client.withSpan(spanName, {}, async () => { + // no op + }); + assert.isTrue( + setAttributeSpy.calledWith("az.namespace", expectedNamespace), + `expected span.setAttribute("az.namespace", "${expectedNamespace}") to have been called` + ); + }); + + it("passes options and span to callback", async () => { + await client.withSpan(spanName, { foo: "foo", bar: "bar" } as any, (options, currentSpan) => { + assert.exists(currentSpan); + assert.exists(options); + assert.equal(options.foo, "foo"); + assert.equal(options.bar, "bar"); + return true; + }); + }); + + it("promisifies synchronous functions", async () => { + const result = await client.withSpan(spanName, {}, () => { + return 5; + }); + assert.equal(result, 5); + }); + + it("supports asynchronous functions", async () => { + const result = await client.withSpan(spanName, {}, () => { + return Promise.resolve(5); + }); + assert.equal(result, 5); + }); + + it("returns context with all existing properties", async () => { + const [key, value] = [Symbol.for("key"), "value"]; + const parentContext = createTracingContext().setValue(key, value); + await client.withSpan( + spanName, + { + tracingOptions: { + tracingContext: parentContext, + }, + }, + (updatedOptions) => { + assert.strictEqual(updatedOptions.tracingOptions.tracingContext.getValue(key), value); + } + ); + }); + + describe("with a successful callback", () => { + it("sets status on the span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setStatusSpy = sinon.spy(span, "setStatus"); + await client.withSpan(spanName, {}, () => Promise.resolve(42)); + + assert.isTrue(setStatusSpy.calledWith(sinon.match({ status: "success" }))); + }); + }); + + describe("with an error", () => { + it("sets status on the span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setStatusSpy = sinon.spy(span, "setStatus"); + let errorThrown = false; + try { + await client.withSpan(spanName, {}, () => Promise.reject(new Error("test"))); + } catch (err) { + errorThrown = true; + assert.isTrue(setStatusSpy.calledWith(sinon.match({ status: "error", error: err }))); + } + + assert.isTrue(errorThrown); + }); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/tracingContext.spec.ts b/sdk/core/core-tracing/test/tracingContext.spec.ts new file mode 100644 index 000000000000..b09087b27fb1 --- /dev/null +++ b/sdk/core/core-tracing/test/tracingContext.spec.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { createDefaultTracingSpan } from "../src/instrumenter"; +import { createTracingClient } from "../src/tracingClient"; +import { TracingContextImpl, createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("TracingContext", () => { + describe("TracingContextImpl", () => { + let context: TracingContextImpl; + + beforeEach(() => { + context = new TracingContextImpl(); + }); + + it("can be created from an existing context map", () => { + const existingContext = createTracingContext() + .setValue(Symbol.for("key1"), "value1") + .setValue(Symbol.for("key2"), "value2"); + const newContext = new TracingContextImpl(existingContext); + assert.equal(newContext.getValue(Symbol.for("key1")), "value1"); + assert.equal(newContext.getValue(Symbol.for("key2")), "value2"); + }); + + describe("getValue and setValue", () => { + it("returns new context with new value", () => { + const newContext = context.setValue(Symbol.for("newKey"), "newVal"); + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("returns new context with all existing values", () => { + const newContext = context + .setValue(Symbol.for("newKey"), "newVal") + .setValue(Symbol.for("someOtherKey"), "someOtherVal") + .setValue(Symbol.for("lastKey"), "lastVal"); + + // inherited context data should remain + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("does not modify existing context", () => { + context.setValue(Symbol.for("newKey"), "newVal"); + assert.notExists(context.getValue(Symbol.for("newKey"))); + }); + + it("can fetch parent chain data", () => { + const newContext = context + .setValue(Symbol.for("ancestry"), "grandparent") + .setValue(Symbol.for("ancestry"), "parent") + .setValue(Symbol.for("self"), "self"); // use a different key for current context + + assert.equal(newContext.getValue(Symbol.for("ancestry")), "parent"); + assert.equal(newContext.getValue(Symbol.for("self")), "self"); + }); + }); + + describe("#deleteValue", () => { + it("returns new context without deleted value", () => { + const newContext = context + .setValue(Symbol.for("newKey"), "newVal") + .deleteValue(Symbol.for("newKey")); + assert.notExists(newContext.getValue(Symbol.for("newKey"))); + }); + + it("does not modify existing context", () => { + const newContext = context.setValue(Symbol.for("newKey"), "newVal"); + newContext.deleteValue(Symbol.for("newKey")); + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("deletes parent chain data", () => { + const newContext = context + .setValue(Symbol.for("ancestry"), "grandparent") + .setValue(Symbol.for("ancestry"), "parent") + .setValue(Symbol.for("self"), "self"); + + assert.isDefined(newContext.getValue(Symbol.for("ancestry"))); + assert.isDefined(newContext.getValue(Symbol.for("self"))); + + const updatedContext = newContext + .deleteValue(Symbol.for("ancestry")) + .deleteValue(Symbol.for("self")); + + assert.isUndefined(updatedContext.getValue(Symbol.for("ancestry"))); + assert.isUndefined(updatedContext.getValue(Symbol.for("self"))); + }); + }); + }); + + describe("#createTracingContext", () => { + it("returns a new context", () => { + const context = createTracingContext(); + assert.exists(context); + assert.instanceOf(context, TracingContextImpl); + }); + + it("can add known attributes", () => { + const client = createTracingClient({ namespace: "test", packageName: "test" }); + const span = createDefaultTracingSpan(); + const namespace = "test-namespace"; + const newContext = createTracingContext({ + client, + span, + namespace, + }); + assert.strictEqual(newContext.getValue(knownContextKeys.client), client); + assert.strictEqual(newContext.getValue(knownContextKeys.namespace), namespace); + assert.strictEqual(newContext.getValue(knownContextKeys.span), span); + }); + + it("can be initialized from an existing context", () => { + const parentContext = createTracingContext().setValue( + knownContextKeys.namespace, + "test-namespace" + ); + const newContext = createTracingContext({ parentContext: parentContext }); + assert.equal(newContext.getValue(knownContextKeys.namespace), "test-namespace"); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/util/testTracerProvider.ts b/sdk/core/core-tracing/test/util/testTracerProvider.ts deleted file mode 100644 index 5d50cbff13f1..000000000000 --- a/sdk/core/core-tracing/test/util/testTracerProvider.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { Tracer, TracerProvider, trace } from "@opentelemetry/api"; -import { TestTracer } from "./testTracer"; - -export class TestTracerProvider implements TracerProvider { - private tracerCache: Map = new Map(); - - getTracer(name: string, version?: string): Tracer { - const tracerKey = `${name}${version}`; - if (!this.tracerCache.has(tracerKey)) { - this.tracerCache.set(tracerKey, new TestTracer(name, version)); - } - return this.tracerCache.get(tracerKey)!; - } - - register(): boolean { - return trace.setGlobalTracerProvider(this); - } - - disable(): void { - trace.disable(); - } -} diff --git a/sdk/instrumentation/ci.yml b/sdk/instrumentation/ci.yml new file mode 100644 index 000000000000..887d18613705 --- /dev/null +++ b/sdk/instrumentation/ci.yml @@ -0,0 +1,30 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - release/* + - hotfix/* + paths: + include: + - sdk/instrumentation/ + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - sdk/instrumentation/ + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: instrumentation + Artifacts: + - name: opentelemetry-instrumentation-azure-sdk + safeName: opentelemetryinstrumentationazuresdk diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc new file mode 100644 index 000000000000..320eddfeffb9 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc @@ -0,0 +1,19 @@ +{ + "include": [ + "dist-esm/src/**/*.js" + ], + "exclude": [ + "**/*.d.ts", + "dist-esm/src/generated/*" + ], + "reporter": [ + "text-summary", + "html", + "cobertura" + ], + "exclude-after-remap": false, + "sourceMap": true, + "produce-source-map": true, + "instrument": true, + "all": true + } diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md new file mode 100644 index 000000000000..4144f75694a0 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md new file mode 100644 index 000000000000..0b9bf89d1a38 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md @@ -0,0 +1,94 @@ +# Azure OpenTelemetry Instrumentation library for JavaScript + +## Getting started + +### Currently supported environments + +- [LTS versions of Node.js](https://nodejs.org/about/releases/) +- Latest versions of Safari, Chrome, Edge, and Firefox. + +See our [support policy](https://github.com/Azure/azure-sdk-for-js/blob/main/SUPPORT.md) for more details. + +### Prerequisites + +- An [Azure subscription][azure_sub]. +- The [@opentelemetry/instrumentation][otel_instrumentation] package. + +You'll need to configure the OpenTelemetry SDK in order to produce Telemetry data. While configuring OpenTelemetry is outside the scope of this README, we encourage you to review the [OpenTelemetry documentation][otel_documentation] in order to get started using OpenTelemetry. + +### Install the `@azure/opentelemetry-instrumentation-azure-sdk` package + +Install the Azure OpenTelemetry Instrumentation client library with `npm`: + +```bash +npm install @azure/opentelemetry-instrumentation-azure-sdk +``` + +### Browser support + +#### JavaScript Bundle + +To use this client library in the browser, first you need to use a bundler. For details on how to do this, please refer to our [bundling documentation](https://aka.ms/AzureSDKBundling). + +## Key concepts + +- The **createAzureSdkInstrumentation** function is the main hook exported by this library which provides a way to create an Azure SDK Instrumentation object to be registered with OpenTelemetry. + +### Compatibility with existing Client Libraries + +- TODO, we should describe what versions of core-tracing are compatible here... + +## Examples + +### Enable OpenTelemetry instrumentation + +```javascript +const { registerInstrumentations } = require("@opentelemetry/instrumentation"); +const { createAzureSdkInstrumentation } = require("@azure/opentelemetry-instrumentation-azure-sdk"); + +// Configure exporters, tracer providers, etc. +// Please refer to the OpenTelemetry documentation for more information. + +registerInstrumentations({ + instrumentations: [createAzureSdkInstrumentation()], +}); + +// Continue to import any Azure SDK client libraries after registering the instrumentation. + +const { keyClient } = require("@azure/keyvault-keys"); + +// Do something cool with the keyClient... +``` + +## Troubleshooting + +### Logging + +Enabling logging may help uncover useful information about failures. In order to see a log of HTTP requests and responses, set the `AZURE_LOG_LEVEL` environment variable to `info`. Alternatively, logging can be enabled at runtime by calling `setLogLevel` in the `@azure/logger`: + +```javascript +import { setLogLevel } from "@azure/logger"; + +setLogLevel("info"); +``` + +For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/logger). + +## Next steps + +- TODO: no samples yet, so the link verification fails. Add link to samples... + +## Contributing + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/main/CONTRIBUTING.md) to learn more about how to build and test the code. + +## Related projects + +- [Microsoft Azure SDK for Javascript](https://github.com/Azure/azure-sdk-for-js) + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Ftemplate%2Ftemplate%2FREADME.png) + +[azure_cli]: https://docs.microsoft.com/cli/azure +[azure_sub]: https://azure.microsoft.com/free/ +[otel_instrumentation]: https://www.npmjs.com/package/@opentelemetry/instrumentation +[otel_documentation]: https://opentelemetry.io/docs/js/ diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json new file mode 100644 index 000000000000..db769b85026a --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/src/index.d.ts", + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/latest/opentelemetry-instrumentation-azure-sdk.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js new file mode 100644 index 000000000000..3156ad1ba1f5 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); +require("dotenv").config(); +const { + jsonRecordingFilterFunction, + isPlaybackMode, + isSoftRecordMode, + isRecordMode, +} = require("@azure-tools/test-recorder"); + +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-junit-reporter", + "karma-json-to-file-reporter", + "karma-json-preprocessor", + ], + + // list of files / patterns to load in the browser + files: [ + "dist-test/index.browser.js", + { pattern: "dist-test/index.browser.js.map", type: "html", included: false, served: true }, + ].concat(isPlaybackMode() || isSoftRecordMode() ? ["recordings/browsers/**/*.json"] : []), + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["env"], + "recordings/browsers/**/*.json": ["json"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + //"dist-test/index.browser.js": ["coverage"] + }, + + envPreprocessor: ["TEST_MODE", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID"], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "junit", "json-to-file"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }], + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {}, // key value pair of properties to add to the section of the report + }, + + jsonToFileReporter: { + filter: jsonRecordingFilterFunction, + outputPath: ".", + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // --no-sandbox allows our tests to run in Linux without having to change the system. + // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex. + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox", "--disable-web-security"], + }, + }, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + browserConsoleLogOptions: { + terminal: !isRecordMode(), + }, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000", + }, + }, + }); +}; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json new file mode 100644 index 000000000000..c1cb0f43e845 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json @@ -0,0 +1,135 @@ +{ + "name": "@azure/opentelemetry-instrumentation-azure-sdk", + "version": "1.0.0-beta.1", + "description": "Instrumentation client for the Azure SDK.", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "browser": { + "./dist-esm/src/instrumentation.js": "./dist-esm/src/instrumentation.browser.js" + }, + "//metadata": { + "constantPaths": [ + { + "path": "src/constants.ts", + "prefix": "SDK_VERSION" + } + ] + }, + "types": "types/latest/opentelemetry-instrumentation-azure-sdk.d.ts", + "typesVersions": { + "<3.6": { + "*": [ + "types/3.1/opentelemetry-instrumentation-azure-sdk.d.ts" + ] + } + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:samples": "echo Obsolete", + "build:test": "tsc -p . && rollup -c 2>&1", + "build:types": "downlevel-dts types/latest/ types/3.1/", + "build": "npm run clean && tsc -p . && rollup -c 2>&1 && api-extractor run --local && npm run build:types", + "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* temp types *.tgz *.log", + "docs": "typedoc --excludePrivate --excludeNotExported --excludeExternals --stripInternal --mode file --out ./dist/docs ./src", + "execute:samples": "dev-tool samples run samples-dev", + "extract-api": "tsc -p . && api-extractor run --local", + "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "generate:client": "autorest --typescript ./swagger/README.md", + "integration-test:browser": "karma start --single-run", + "integration-test:node": "nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 5000000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "test:browser": "npm run clean && npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run clean && tsc -p . && npm run unit-test:node && npm run integration-test:node", + "test": "npm run clean && tsc -p . && npm run unit-test:node && rollup -c 2>&1 && npm run unit-test:browser && npm run integration-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha -r esm -r ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace --exclude \"test/**/browser/*.spec.ts\" \"test/**/*.spec.ts\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/latest/", + "types/3.1/", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "engines": { + "node": ">=12.0.0" + }, + "keywords": [ + "azure", + "cloud", + "tracing", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/core-tracing": "1.0.0-preview.14", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.0.3", + "@opentelemetry/core": "^1.0.1", + "@opentelemetry/instrumentation": "^0.27.0", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@azure-tools/test-recorder": "^1.0.0", + "@azure/dev-tool": "^1.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@microsoft/api-extractor": "^7.18.11", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^12.0.0", + "@types/sinon": "^10.0.6", + "chai": "^4.2.0", + "cross-env": "^7.0.2", + "dotenv": "^8.2.0", + "downlevel-dts": "~0.4.0", + "eslint": "^7.15.0", + "esm": "^3.2.18", + "inherits": "^2.0.3", + "karma": "^6.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "nyc": "^14.0.0", + "prettier": "^2.5.1", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "sinon": "^12.0.1", + "source-map-support": "^0.5.9", + "typedoc": "0.15.2", + "typescript": "~4.2.0", + "util": "^0.12.1" + }, + "//sampleConfiguration": { + "skipFolder": true, + "disableDocsMs": true, + "productName": "Azure OpenTelemetry Instrumentation", + "productSlugs": [], + "apiRefLink": "https://docs.microsoft.com/javascript/api/", + "requiredResources": {} + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md new file mode 100644 index 000000000000..6c381f39eed2 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md @@ -0,0 +1,23 @@ +## API Report File for "@azure/opentelemetry-instrumentation-azure-sdk" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AzureLogger } from '@azure/logger'; +import { Instrumentation } from '@opentelemetry/instrumentation'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +// @public +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig { +} + +// @public +export function createAzureSdkInstrumentation(options?: AzureSdkInstrumentationOptions): Instrumentation; + +// @public +export const logger: AzureLogger; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js new file mode 100644 index 000000000000..5d7deee44c14 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js @@ -0,0 +1,3 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; + +export default makeConfig(require("./package.json")); diff --git a/sdk/core/core-tracing/src/utils/browser.d.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts similarity index 53% rename from sdk/core/core-tracing/src/utils/browser.d.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts index 72f5b34bc8cc..47dc16dd0f7c 100644 --- a/sdk/core/core-tracing/src/utils/browser.d.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts @@ -1,5 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -interface Window {} -declare let self: Window & typeof globalThis; +export const SDK_VERSION: string = "1.0.0-beta.1"; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts new file mode 100644 index 000000000000..c184a0d70b4c --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export * from "./logger"; +export * from "./instrumentation"; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts new file mode 100644 index 000000000000..b74fedddb12d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumentation, + InstrumentationBase, + InstrumentationConfig, +} from "@opentelemetry/instrumentation"; +import { SDK_VERSION } from "./constants"; +import { useInstrumenter } from "@azure/core-tracing"; +import { OpenTelemetryInstrumenter } from "./instrumenter"; + +/** + * Configuration options that can be passed to {@link createAzureSdkInstrumentation} function. + */ +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig {} + +/** + * The instrumentation module for the Azure SDK. Implements OpenTelemetry's {@link Instrumentation}. + */ +class AzureSdkInstrumentation extends InstrumentationBase { + constructor(options: AzureSdkInstrumentationOptions = {}) { + super( + "@azure/opentelemetry-instrumentation-azure-sdk", + SDK_VERSION, + Object.assign({}, options) + ); + } + /** In the browser we rely on overriding the `enable` function instead as there are no modules to patch. */ + protected init(): void { + // no-op + } + + /** + * Entrypoint for the module registration. Ensures the global instrumenter is set to use OpenTelemetry. + */ + enable(): void { + useInstrumenter(new OpenTelemetryInstrumenter()); + } + + disable(): void { + // no-op + } +} + +/** + * Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries. + * + * When registerd, any Azure data plane package will begin emitting tracing spans for internal calls + * as well as network calls + * + * Example usage: + * ```ts + * const openTelemetryInstrumentation = require("@opentelemetry/instrumentation"); + * openTelemetryInstrumentation.registerInstrumentations({ + * instrumentations: [createAzureSdkInstrumentation()], + * }) + * ``` + */ +export function createAzureSdkInstrumentation( + options: AzureSdkInstrumentationOptions = {} +): Instrumentation { + return new AzureSdkInstrumentation(options); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts new file mode 100644 index 000000000000..086460049529 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumentation, + InstrumentationBase, + InstrumentationConfig, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, +} from "@opentelemetry/instrumentation"; +import type * as coreTracing from "@azure/core-tracing"; +import { OpenTelemetryInstrumenter } from "./instrumenter"; +import { SDK_VERSION } from "./constants"; + +/** + * Configuration options that can be passed to {@link createAzureSdkInstrumentation} function. + */ +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig {} + +/** + * The instrumentation module for the Azure SDK. Implements OpenTelemetry's {@link Instrumentation}. + */ +class AzureSdkInstrumentation extends InstrumentationBase { + constructor(options: AzureSdkInstrumentationOptions = {}) { + super( + "@azure/opentelemetry-instrumentation-azure-sdk", + SDK_VERSION, + Object.assign({}, options) + ); + } + /** + * Entrypoint for the module registration. + * + * @returns The patched \@azure/core-tracing module after setting its instrumenter. + */ + protected init(): + | void + | InstrumentationModuleDefinition + | InstrumentationModuleDefinition[] { + const result: InstrumentationModuleDefinition = + new InstrumentationNodeModuleDefinition( + "@azure/core-tracing", + ["^1.0.0-preview.14", "^1.0.0"], + (moduleExports) => { + if (typeof moduleExports.useInstrumenter === "function") { + moduleExports.useInstrumenter(new OpenTelemetryInstrumenter()); + } + + return moduleExports; + } + ); + // Needed to support 1.0.0-preview.14 + result.includePrerelease = true; + return result; + } +} + +/** + * Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries. + * + * When registerd, any Azure data plane package will begin emitting tracing spans for internal calls + * as well as network calls + * + * Example usage: + * ```ts + * const openTelemetryInstrumentation = require("@opentelemetry/instrumentation"); + * openTelemetryInstrumentation.registerInstrumentations({ + * instrumentations: [createAzureSdkInstrumentation()], + * }) + * ``` + * + * @remarks + * + * As OpenTelemetry instrumentations rely on patching required modules, you should register + * this instrumentation as early as possible and before loading any Azure Client Libraries. + */ +export function createAzureSdkInstrumentation( + options: AzureSdkInstrumentationOptions = {} +): Instrumentation { + return new AzureSdkInstrumentation(options); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts new file mode 100644 index 000000000000..8cd86717849d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumenter, + InstrumenterSpanOptions, + TracingContext, + TracingSpan, +} from "@azure/core-tracing"; + +import { trace, context, defaultTextMapGetter, defaultTextMapSetter } from "@opentelemetry/api"; +import { W3CTraceContextPropagator } from "@opentelemetry/core"; +import { OpenTelemetrySpanWrapper } from "./spanWrapper"; + +import { toSpanOptions } from "./transformations"; + +// While default propagation is user-configurable, Azure services always use the W3C implementation. +export const propagator = new W3CTraceContextPropagator(); + +export class OpenTelemetryInstrumenter implements Instrumenter { + startSpan( + name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } { + const span = trace + .getTracer(spanOptions.packageName, spanOptions.packageVersion) + .startSpan(name, toSpanOptions(spanOptions)); + + const ctx = spanOptions?.tracingContext || context.active(); + + return { + span: new OpenTelemetrySpanWrapper(span), + tracingContext: trace.setSpan(ctx, span), + }; + } + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + tracingContext: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return context.with( + tracingContext, + callback, + /** Assume caller will bind `this` or use arrow functions */ undefined, + ...callbackArgs + ); + } + + parseTraceparentHeader(traceparentHeader: string): TracingContext { + return propagator.extract( + context.active(), + { traceparent: traceparentHeader }, + defaultTextMapGetter + ); + } + + createRequestHeaders(tracingContext?: TracingContext): Record { + const headers: Record = {}; + propagator.inject(tracingContext || context.active(), headers, defaultTextMapSetter); + return headers; + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts new file mode 100644 index 000000000000..73fd29e1fd26 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createClientLogger } from "@azure/logger"; + +/** + * The \@azure/logger configuration for this package. + */ +export const logger = createClientLogger("opentelemetry-instrumentation-azure-sdk"); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts new file mode 100644 index 000000000000..3555f83e9356 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SpanStatus, TracingSpan } from "@azure/core-tracing"; +import { Span, SpanStatusCode, SpanAttributeValue } from "@opentelemetry/api"; + +export class OpenTelemetrySpanWrapper implements TracingSpan { + private _span: Span; + + constructor(span: Span) { + this._span = span; + } + + setStatus(status: SpanStatus): void { + if (status.status === "error") { + if (status.error) { + this._span.setStatus({ code: SpanStatusCode.ERROR, message: status.error.toString() }); + this.recordException(status.error); + } else { + this._span.setStatus({ code: SpanStatusCode.ERROR }); + } + } else if (status.status === "success") { + this._span.setStatus({ code: SpanStatusCode.OK }); + } + } + + setAttribute(name: string, value: unknown): void { + if (value !== null && value !== undefined) { + this._span.setAttribute(name, value as SpanAttributeValue); + } + } + + end(): void { + this._span.end(); + } + + recordException(exception: string | Error): void { + this._span.recordException(exception); + } + + isRecording(): boolean { + return this._span.isRecording(); + } + + /** + * Allows getting the wrapped span as needed. + * @internal + * + * @returns The underlying span + */ + unwrap(): Span { + return this._span; + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts new file mode 100644 index 000000000000..2bc9ad8926c0 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { InstrumenterSpanOptions, TracingSpanKind, TracingSpanLink } from "@azure/core-tracing"; +import { + Link, + SpanAttributeValue, + SpanAttributes, + SpanKind, + SpanOptions, + trace, +} from "@opentelemetry/api"; + +/** + * Converts our TracingSpanKind to the corresponding OpenTelemetry SpanKind. + * + * By default it will return {@link SpanKind.INTERNAL} + * @param tracingSpanKind - The core tracing {@link TracingSpanKind} + * @returns - The OpenTelemetry {@link SpanKind} + */ +export function toOpenTelemetrySpanKind( + tracingSpanKind?: K +): SpanKindMapping[K] { + const key = (tracingSpanKind || "internal").toUpperCase() as keyof typeof SpanKind; + return SpanKind[key] as SpanKindMapping[K]; +} + +/** + * A mapping between our {@link TracingSpanKind} union type and OpenTelemetry's {@link SpanKind}. + */ +type SpanKindMapping = { + client: SpanKind.CLIENT; + server: SpanKind.SERVER; + producer: SpanKind.PRODUCER; + consumer: SpanKind.CONSUMER; + internal: SpanKind.INTERNAL; +}; + +/** + * Converts core-tracing's TracingSpanLink to OpenTelemetry's Link + * + * @param spanLinks - The core tracing {@link TracingSpanLink} to convert + * @returns A set of {@link Link}s + */ +function toOpenTelemetryLinks(spanLinks: TracingSpanLink[] = []): Link[] { + return spanLinks.reduce((acc, tracingSpanLink) => { + const spanContext = trace.getSpanContext(tracingSpanLink.tracingContext); + if (spanContext) { + acc.push({ + context: spanContext, + attributes: toOpenTelemetrySpanAttributes(tracingSpanLink.attributes), + }); + } + return acc; + }, [] as Link[]); +} + +/** + * Converts core-tracing's span attributes to OpenTelemetry attributes. + * + * @param spanAttributes - The set of attributes to convert. + * @returns An {@link SpanAttributes} to set on a span. + */ +function toOpenTelemetrySpanAttributes( + spanAttributes: { [key: string]: unknown } | undefined +): SpanAttributes { + const attributes: ReturnType = {}; + for (const key in spanAttributes) { + // Any non-nullish value is allowed. + if (spanAttributes[key] !== null && spanAttributes[key] !== undefined) { + attributes[key] = spanAttributes[key] as SpanAttributeValue; + } + } + return attributes; +} + +/** + * Converts core-tracing span options to OpenTelemetry options. + * + * @param spanOptions - The {@link InstrumenterSpanOptions} to convert. + * @returns An OpenTelemetry {@link SpanOptions} that can be used when creating a span. + */ +export function toSpanOptions(spanOptions?: InstrumenterSpanOptions): SpanOptions { + const { spanAttributes, spanLinks, spanKind } = spanOptions || {}; + + const attributes: SpanAttributes = toOpenTelemetrySpanAttributes(spanAttributes); + const kind = toOpenTelemetrySpanKind(spanKind); + const links = toOpenTelemetryLinks(spanLinks); + + return { + attributes, + kind, + links, + }; +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts new file mode 100644 index 000000000000..0fc493c5d5df --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { OpenTelemetryInstrumenter, propagator } from "../../src/instrumenter"; +import { trace, context, SpanKind } from "@opentelemetry/api"; +import { TracingSpan, TracingSpanKind } from "@azure/core-tracing"; +import { TestSpan } from "./util/testSpan"; +import { TestTracer } from "./util/testTracer"; +import { resetTracer, setTracer } from "./util/testTracerProvider"; +import sinon from "sinon"; +import { Context } from "mocha"; +import { OpenTelemetrySpanWrapper } from "../../src/spanWrapper"; + +function unwrap(span: TracingSpan): TestSpan { + return (span as OpenTelemetrySpanWrapper).unwrap() as TestSpan; +} + +describe("OpenTelemetryInstrumenter", () => { + const instrumenter = new OpenTelemetryInstrumenter(); + + describe("#createRequestHeaders", () => { + afterEach(() => { + sinon.restore(); + }); + + it("uses the passed in context if it exists", () => { + let propagationSpy = sinon.spy(propagator); + const span = new TestTracer().startSpan("test"); + let tracingContext = trace.setSpan(context.active(), span); + instrumenter.createRequestHeaders(tracingContext); + assert.isTrue(propagationSpy.inject.calledWith(tracingContext)); + }); + + it("uses the active context if no context was provided", () => { + let propagationSpy = sinon.spy(propagator); + instrumenter.createRequestHeaders(); + const activeContext = context.active(); + assert.isTrue(propagationSpy.inject.calledWith(activeContext)); + }); + }); + + // TODO: the following still uses existing test support for OTel. + // Once the new APIs are available we should move away from those. + describe("#startSpan", () => { + let tracer: TestTracer; + const packageName = "test-package"; + const packageVersion = "test-version"; + beforeEach(() => { + tracer = setTracer(tracer); + }); + + afterEach(() => { + resetTracer(); + }); + + it("returns a newly started TracingSpan", () => { + const { span } = instrumenter.startSpan("test", { packageName, packageVersion }); + const otSpan = unwrap(span); + assert.equal(otSpan, tracer.getActiveSpans()[0]); + assert.equal(otSpan.kind, SpanKind.INTERNAL); + }); + + it("passes package information to the tracer", () => { + const getTracerSpy = sinon.spy(trace, "getTracer"); + instrumenter.startSpan("test", { packageName, packageVersion }); + + assert.isTrue(getTracerSpy.calledWith(packageName, packageVersion)); + }); + + describe("with an existing context", () => { + it("returns a context that contains all existing fields", () => { + const currentContext = context.active().setValue(Symbol.for("foo"), "bar"); + + const { tracingContext } = instrumenter.startSpan("test", { + tracingContext: currentContext, + packageName, + }); + + assert.equal(tracingContext.getValue(Symbol.for("foo")), "bar"); + }); + + it("sets span on the context", () => { + const currentContext = context.active().setValue(Symbol.for("foo"), "bar"); + + const { span, tracingContext } = instrumenter.startSpan("test", { + tracingContext: currentContext, + packageName, + }); + + assert.equal(trace.getSpan(tracingContext), unwrap(span)); + }); + }); + + describe("when a context is not provided", () => { + it("uses the active context", () => { + const contextSpy = sinon.spy(context, "active"); + + instrumenter.startSpan("test", { packageName, packageVersion }); + + assert.isTrue(contextSpy.called); + }); + + it("sets span on the context", () => { + const { span, tracingContext } = instrumenter.startSpan("test", { + packageName, + packageVersion, + }); + + assert.equal(trace.getSpan(tracingContext), unwrap(span)); + }); + }); + + describe("spanOptions", () => { + it("passes attributes to started span", () => { + const spanAttributes = { + attr1: "val1", + attr2: "val2", + }; + const { span } = instrumenter.startSpan("test", { + spanAttributes, + packageName, + packageVersion, + }); + + assert.deepEqual(unwrap(span).attributes, spanAttributes); + }); + + describe("spanKind", () => { + it("maps spanKind correctly", () => { + const { span } = instrumenter.startSpan("test", { + packageName, + spanKind: "client", + }); + assert.equal(unwrap(span).kind, SpanKind.CLIENT); + }); + + it("defaults spanKind to INTERNAL if omitted", () => { + const { span } = instrumenter.startSpan("test", { packageName }); + assert.equal(unwrap(span).kind, SpanKind.INTERNAL); + }); + + // TODO: what's the right behavior? throw? log and continue? + it("defaults spanKind to INTERNAL if an invalid spanKind is provided", () => { + const { span } = instrumenter.startSpan("test", { + packageName, + spanKind: "foo" as TracingSpanKind, + }); + assert.equal(unwrap(span).kind, SpanKind.INTERNAL); + }); + }); + + it("supports spanLinks", () => { + const { tracingContext: linkedSpanContext } = instrumenter.startSpan("linked", { + packageName, + }); + + const { span } = instrumenter.startSpan("test", { + packageName, + spanLinks: [ + { + tracingContext: linkedSpanContext, + attributes: { + attr1: "value1", + }, + }, + ], + }); + + const links = unwrap(span).links; + assert.equal(links.length, 1); + assert.deepEqual(links[0].attributes, { attr1: "value1" }); + assert.deepEqual(links[0].context, trace.getSpan(linkedSpanContext)?.spanContext()); + }); + + it("supports spanLinks from traceparentHeader", () => { + const linkedContext = instrumenter.parseTraceparentHeader( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ); + + const { span } = instrumenter.startSpan("test", { + packageName, + spanLinks: [{ tracingContext: linkedContext! }], + }); + + const links = unwrap(span).links; + assert.equal(links.length, 1); + assert.deepEqual(links[0].context, trace.getSpan(linkedContext!)?.spanContext()); + }); + }); + }); + + describe("#withContext", () => { + it("passes the correct arguments to OpenTelemetry", function (this: Context) { + const contextSpy = sinon.spy(context, "with"); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const callback = (arg1: number) => arg1 + 42; + const callbackArg = 37; + const activeContext = context.active(); + instrumenter.withContext(activeContext, callback, callbackArg); + + assert.isTrue(contextSpy.calledWith(activeContext, callback, undefined, callbackArg)); + }); + + it("works when caller binds `this`", function (this: Context) { + // a bit of a silly test but demonstrates how to bind `this` correctly + // and ensures the behavior does not regress + + // Function syntax + instrumenter.withContext(context.active(), function (this: any) { + assert.isUndefined(this); + }); + instrumenter.withContext( + context.active(), + function (this: any) { + assert.equal(this, 42); + }.bind(42) + ); + + // Arrow syntax + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + instrumenter.withContext(context.active(), () => { + assert.equal(this, that); + }); + }); + + it("Returns the value of the callback", () => { + const result = instrumenter.withContext(context.active(), () => 42); + assert.equal(result, 42); + }); + }); + + describe("#parseTraceparentHeader", () => { + it("returns a new context with spanContext set", () => { + const validTraceparentHeader = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + const updatedContext = instrumenter.parseTraceparentHeader(validTraceparentHeader); + assert.exists(updatedContext); + const spanContext = trace.getSpanContext(updatedContext!); + assert.equal(spanContext?.spanId, "00f067aa0ba902b7"); + assert.equal(spanContext?.traceId, "4bf92f3577b34da6a3ce929d0e0e4736"); + }); + }); +}); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts new file mode 100644 index 000000000000..f7a362433745 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { OpenTelemetrySpanWrapper } from "../../src/spanWrapper"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { TestSpan } from "./util/testSpan"; +import { TestTracer } from "./util/testTracer"; + +describe("OpenTelemetrySpanWrapper", () => { + let otSpan: TestSpan; + let span: OpenTelemetrySpanWrapper; + + beforeEach(() => { + otSpan = new TestTracer().startSpan("test"); + span = new OpenTelemetrySpanWrapper(otSpan); + }); + + describe("#setStatus", () => { + describe("with a successful status", () => { + it("sets the status on the span", () => { + span.setStatus({ status: "success" }); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.OK }); + }); + }); + + describe("with an error", () => { + it("sets the failed status on the span", () => { + span.setStatus({ status: "error" }); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.ERROR }); + }); + + it("records the exception if provided", () => { + const error = new Error("test"); + span.setStatus({ status: "error", error }); + + assert.deepEqual(otSpan.exception, error); + }); + }); + }); + + describe("#setAttribute", () => { + it("records the attribute on the span", () => { + span.setAttribute("test", "value"); + span.setAttribute("array", ["value"]); + + assert.deepEqual(otSpan.attributes, { test: "value", array: ["value"] }); + }); + + it("ignores null", () => { + span.setAttribute("test", null); + + assert.isEmpty(otSpan.attributes); + }); + + it("ignores undefined", () => { + span.setAttribute("test", undefined); + + assert.isEmpty(otSpan.attributes); + }); + }); + + describe("#end", () => { + it("ends the wrapped span", () => { + span.end(); + + assert.isTrue(otSpan.endCalled); + }); + }); + + describe("#recordException", () => { + it("sets the error on the wrapped span", () => { + const error = new Error("test"); + span.recordException(error); + + assert.deepEqual(otSpan.exception, error); + }); + it("does not change the status", () => { + const error = "test"; + span.recordException(error); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.UNSET }); + }); + }); + + describe("#isRecording", () => { + it("returns the value of the wrapped span", () => { + assert.equal(span.isRecording(), otSpan.isRecording()); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/util/testSpan.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts similarity index 84% rename from sdk/core/core-tracing/test/util/testSpan.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts index 8ff22477ceb9..95f131aec018 100644 --- a/sdk/core/core-tracing/test/util/testSpan.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts @@ -2,17 +2,17 @@ // Licensed under the MIT license. import { - Span, - SpanAttributeValue, - SpanAttributes, - SpanContext, + TimeInput, + Tracer, SpanKind, - SpanOptions, SpanStatus, + SpanContext, + SpanAttributes, SpanStatusCode, - TimeInput, - Tracer, -} from "../../src/interfaces"; + SpanAttributeValue, + Span, + Link, +} from "@opentelemetry/api"; /** * A mock span useful for testing. @@ -56,6 +56,16 @@ export class TestSpan implements Span { private _context: SpanContext; private readonly _tracer: Tracer; + /** + * The recorded exception, if any. + */ + exception?: Error; + + /** + * Any links provided when creating this span. + */ + links: Link[]; + /** * Starts a new Span. * @param parentTracer- The tracer that created this Span @@ -69,20 +79,24 @@ export class TestSpan implements Span { parentTracer: Tracer, name: string, context: SpanContext, + kind: SpanKind, parentSpanId?: string, - options?: SpanOptions + startTime: TimeInput = Date.now(), + attributes: SpanAttributes = {}, + links: Link[] = [] ) { this._tracer = parentTracer; this.name = name; - this.kind = options?.kind || SpanKind.INTERNAL; - this.startTime = options?.startTime || Date.now(); + this.kind = kind; + this.startTime = startTime; this.parentSpanId = parentSpanId; - this.attributes = options?.attributes || {}; this.status = { - code: SpanStatusCode.OK, + code: SpanStatusCode.UNSET, }; this.endCalled = false; this._context = context; + this.attributes = attributes; + this.links = links; } /** @@ -148,8 +162,8 @@ export class TestSpan implements Span { addEvent(): this { throw new Error("Method not implemented."); } - recordException(): void { - throw new Error("Method not implemented."); + recordException(exception: Error): void { + this.exception = exception; } updateName(): this { throw new Error("Method not implemented."); diff --git a/sdk/core/core-tracing/test/util/testTracer.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts similarity index 88% rename from sdk/core/core-tracing/test/util/testTracer.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts index 1b9d6102aa88..ccdfbca28a00 100644 --- a/sdk/core/core-tracing/test/util/testTracer.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { TestSpan } from "./testSpan"; import { - Context as OTContext, SpanContext, + SpanKind, SpanOptions, TraceFlags, + Context, + context, Tracer, - getSpanContext, - context as otContext, -} from "../../src/interfaces"; -import { TestSpan } from "./testSpan"; + trace, +} from "@opentelemetry/api"; /** * Simple representation of a Span that only has name and child relationships. @@ -124,8 +125,8 @@ export class TestTracer implements Tracer { * @param name - The name of the span. * @param options - The SpanOptions used during Span creation. */ - startSpan(name: string, options?: SpanOptions, context?: OTContext): TestSpan { - const parentContext = getSpanContext(context || otContext.active()); + startSpan(name: string, options?: SpanOptions, currentContext?: Context): TestSpan { + const parentContext = trace.getSpanContext(currentContext || context.active()); let traceId: string; let isRootSpan = false; @@ -142,7 +143,16 @@ export class TestTracer implements Tracer { spanId: this.getNextSpanId(), traceFlags: TraceFlags.NONE, }; - const span = new TestSpan(this, name, spanContext, parentContext?.spanId, options); + const span = new TestSpan( + this, + name, + spanContext, + options?.kind || SpanKind.INTERNAL, + parentContext ? parentContext.spanId : undefined, + options?.startTime, + options?.attributes, + options?.links + ); this.knownSpans.push(span); if (isRootSpan) { this.rootSpans.push(span); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts new file mode 100644 index 000000000000..cca68d106517 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracerProvider, trace } from "@opentelemetry/api"; +import { TestTracer } from "./testTracer"; + +export class TestTracerProvider implements TracerProvider { + private tracer = new TestTracer(); + + getTracer(): TestTracer { + return this.tracer; + } + + register(): boolean { + return trace.setGlobalTracerProvider(this); + } + + disable(): void { + trace.disable(); + } + + setTracer(tracer: TestTracer): void { + this.tracer = tracer; + } +} + +let tracerProvider: TestTracerProvider; + +export function setTracer(tracer?: TestTracer): TestTracer { + resetTracer(); + tracerProvider = new TestTracerProvider(); + tracerProvider.register(); + if (tracer) { + tracerProvider.setTracer(tracer); + } + return tracerProvider.getTracer(); +} + +export function resetTracer(): void { + tracerProvider?.disable(); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml new file mode 100644 index 000000000000..cbf7ddd903d8 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml @@ -0,0 +1,11 @@ +trigger: none + +stages: + - template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml + parameters: + PackageName: "@azure/opentelemetry-instrumentation-azure-sdk" + ServiceDirectory: instrumentation + EnvVars: + AZURE_CLIENT_ID: $(aad-azure-sdk-test-client-id) + AZURE_TENANT_ID: $(aad-azure-sdk-test-tenant-id) + AZURE_CLIENT_SECRET: $(aad-azure-sdk-test-client-secret) diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json new file mode 100644 index 000000000000..072f6025f15d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types", + "paths": { + "@azure/opentelemetry-instrumentation-azure-sdk": ["./src/index"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts", "samples-dev/**/*.ts"] +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json new file mode 100644 index 000000000000..81c5a8a2aa2f --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../tsdoc.json"] +} diff --git a/sdk/test-utils/test-utils/package.json b/sdk/test-utils/test-utils/package.json index 9fa9106ad390..98d3ada24c32 100644 --- a/sdk/test-utils/test-utils/package.json +++ b/sdk/test-utils/test-utils/package.json @@ -59,13 +59,13 @@ "mocha": "^7.1.1", "@azure-tools/test-recorder": "^1.0.0", "tslib": "^2.2.0", - "@azure/core-tracing": "1.0.0-preview.13" + "@azure/core-tracing": "1.0.0-preview.14", + "@opentelemetry/api": "^1.0.3" }, "devDependencies": { "@azure/dev-tool": "^1.0.0", "@azure/eslint-plugin-azure-sdk": "^3.0.0", "@microsoft/api-extractor": "^7.18.11", - "@opentelemetry/api": "^1.0.1", "@types/chai": "^4.1.6", "@types/mocha": "^7.0.2", "@types/node": "^12.0.0", diff --git a/sdk/test-utils/test-utils/src/index.ts b/sdk/test-utils/test-utils/src/index.ts index 7398906fbb16..0a4ce96c90d9 100644 --- a/sdk/test-utils/test-utils/src/index.ts +++ b/sdk/test-utils/test-utils/src/index.ts @@ -9,10 +9,14 @@ export { TestFunctionWrapper, } from "./multiVersion"; +export { chaiAzureTrace } from "./tracing/chaiAzureTrace"; export { matrix } from "./matrix"; export { isNode, isNode8 } from "./utils"; export { getYieldedValue } from "./getYieldedValue"; - export { TestSpan } from "./tracing/testSpan"; + +export * from "./tracing/mockInstrumenter"; +export * from "./tracing/mockTracingSpan"; export * from "./tracing/testTracer"; export * from "./tracing/testTracerProvider"; +export * from "./tracing/spanGraphModel"; diff --git a/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts new file mode 100644 index 000000000000..6cc52dc60f30 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { OperationTracingOptions, useInstrumenter } from "@azure/core-tracing"; +import { assert } from "chai"; +import { MockInstrumenter } from "./mockInstrumenter"; +import { MockTracingSpan } from "./mockTracingSpan"; +import { SpanGraph, SpanGraphNode } from "./spanGraphModel"; + +/** + * Augments Chai with support for Azure Tracing functionality + * + * Sample usage: + * + * ```ts + * import chai from "chai"; + * import { chaiAzureTrace } from "@azure/test-utils"; + * chai.use(chaiAzureTrace); + * + * it("supportsTracing", async () => { + * await assert.supportsTracing((updatedOptions) => myClient.doSomething(updatedOptions), ["myClient.doSomething"]); + * }); + * ``` + * @param chai - The Chai instance + */ +function chaiAzureTrace(chai: Chai.ChaiStatic): void { + // expect(() => {}).to.supportsTracing() syntax + chai.Assertion.addMethod("supportTracing", function < + T + >(this: Chai.AssertionStatic, expectedSpanNames: string[], options?: T) { + return assert.supportsTracing(this._obj, expectedSpanNames, options, this._obj); + }); + + // assert.supportsTracing(() => {}) syntax + chai.assert.supportsTracing = supportsTracing; +} + +const instrumenter = new MockInstrumenter(); +/** + * The supports Tracing function does the verification of whether the core-tracing is supported correctly with the client method + * This function verifies the root span, if all the correct spans are called as expected and if they are closed. + * @param callback - Callback function of the client that should be invoked + * @param expectedSpanNames - List of span names that are expected to be generated + * @param options - Options for either Core HTTP operations or custom options for the callback + * @param thisArg - optional this parameter for the callback + */ +async function supportsTracing< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends (options: Options) => Promise +>( + callback: Callback, + expectedSpanNames: string[], + options?: Options, + thisArg?: ThisParameterType +) { + useInstrumenter(instrumenter); + instrumenter.reset(); + 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 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.` + ); +} + +/** + * Return all Spans for a particular trace, grouped by their + * parent Span in a tree-like structure + * @param traceId - The traceId to return the graph for + */ +function getSpanGraph(traceId: string, instrumenter: MockInstrumenter): SpanGraph { + const traceSpans = instrumenter.startedSpans.filter((span) => { + return span.traceId === traceId; + }); + + const roots: SpanGraphNode[] = []; + const nodeMap: Map = new Map(); + + for (const span of traceSpans) { + const spanId = span.spanId; + const node: SpanGraphNode = { + name: span.name, + children: [], + }; + nodeMap.set(spanId, node); + + if (span.parentSpan()?.spanId) { + const parentSpan = span.parentSpan()?.spanId; + const parent = nodeMap.get(parentSpan!); + if (!parent) { + throw new Error( + `Span with name ${node.name} has an unknown parentSpan with id ${parentSpan}` + ); + } + parent.children.push(node); + } else { + roots.push(node); + } + } + + return { + roots, + }; +} + +/* eslint-disable @typescript-eslint/no-namespace */ +declare global { + export namespace Chai { + interface Assertion { + supportTracing(expectedSpanNames: string[], options?: T): Promise; + } + interface Assert { + supportsTracing< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends (options: Options) => Promise + >( + callback: Callback, + expectedSpanNames: string[], + options?: Options, + thisArg?: ThisParameterType + ): Promise; + } + } +} + +export { chaiAzureTrace }; diff --git a/sdk/test-utils/test-utils/src/tracing/mockContext.ts b/sdk/test-utils/test-utils/src/tracing/mockContext.ts new file mode 100644 index 000000000000..972666a643f5 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracingContext } from "@azure/core-tracing"; + +/** + * This is the implementation of the {@link TracingContext} interface + * Represents a tracing context + */ +export class MockContext implements TracingContext { + /** + * Represents a context map for the symbols to record + */ + private contextMap: Map; + + /** + * Initializes the context map + * @param parentContext - If present the context map is initialized to the contextMap of the parentContext + */ + constructor(parentContext?: TracingContext) { + if (parentContext && !(parentContext instanceof MockContext)) { + throw new Error("received parent context, but it is not mock context..."); + } + this.contextMap = new Map(parentContext?.contextMap || new Map()); + } + + setValue(key: symbol, value: unknown): TracingContext { + const newContext = new MockContext(this); + newContext.contextMap.set(key, value); + return newContext; + } + + getValue(key: symbol): unknown { + return this.contextMap.get(key); + } + + deleteValue(key: symbol): TracingContext { + const newContext = new MockContext(this); + newContext.contextMap.delete(key); + return newContext; + } +} + +export const spanKey = Symbol.for("span"); diff --git a/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts new file mode 100644 index 000000000000..e7d55690ec0d --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumenter, + InstrumenterSpanOptions, + TracingContext, + TracingSpan, +} from "@azure/core-tracing"; +import { MockContext, spanKey } from "./mockContext"; +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 { + /** + * Stack of immutable contexts, each of which is a bag of tracing values for the current operation + */ + public contextStack: TracingContext[] = [new MockContext()]; + /** + * List of started spans + */ + public startedSpans: MockTracingSpan[] = []; + + private traceIdCounter = 0; + private getNextTraceId(): string { + this.traceIdCounter++; + return this.traceIdCounter.toString().padStart(32, "0"); + } + + private spanIdCounter = 0; + private getNextSpanId(): string { + this.spanIdCounter++; + return this.spanIdCounter.toString().padStart(16, "0"); + } + + startSpan( + name: string, + spanOptions?: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } { + const tracingContext = spanOptions?.tracingContext || this.currentContext(); + const parentSpan = tracingContext.getValue(spanKey) as MockTracingSpan | undefined; + let traceId; + if (parentSpan) { + traceId = parentSpan.traceId; + } else { + traceId = this.getNextTraceId(); + } + + const spanContext = { + spanId: this.getNextSpanId(), + traceId: traceId, + traceFlags: 0, + }; + const span = new MockTracingSpan( + name, + spanContext.traceId, + spanContext.spanId, + tracingContext, + spanOptions + ); + let context: TracingContext = new MockContext(tracingContext); + context = context.setValue(spanKey, span); + + this.startedSpans.push(span); + return { span, tracingContext: context }; + } + + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + this.contextStack.push(context); + return Promise.resolve(callback(...callbackArgs)).finally(() => { + this.contextStack.pop(); + }) as ReturnType; + } + + parseTraceparentHeader(_traceparentHeader: string): TracingContext | undefined { + return; + } + + createRequestHeaders(_tracingContext: TracingContext): Record { + return {}; + } + + /** + * Gets the currently active context. + * + * @returns The current context. + */ + currentContext() { + return this.contextStack[this.contextStack.length - 1]; + } + + /** + * Resets the state of the instrumenter to a clean slate. + */ + reset() { + this.contextStack = [new MockContext()]; + this.startedSpans = []; + this.traceIdCounter = 0; + this.spanIdCounter = 0; + } +} diff --git a/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts new file mode 100644 index 000000000000..4ebcfd905f0b --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + TracingSpan, + SpanStatus, + TracingSpanOptions, + TracingSpanKind, + TracingContext, +} from "@azure/core-tracing"; +import { spanKey } from "./mockContext"; + +/** + * Represents an implementation of a mock tracing span {@link TracingSpan} used for tests + */ +export class MockTracingSpan implements TracingSpan { + /** + * Name of the current span + */ + name: string; + /** + * Kind of the current span {@link TracingSpanKind} + */ + spanKind?: TracingSpanKind; + /** + * Existing or parent tracing context + */ + tracingContext?: TracingContext; + + /** + * The generated ID of the span within a given trace + */ + spanId: string; + + /** + * The ID of the trace this span belongs to + */ + traceId: string; + + /** + * + * @param name - Name of the current span + * @param spanContext - A unique, serializable identifier for a span + * @param tracingContext - Existing or parent tracing context + * @param spanOptions - Options to configure the newly created span {@link TracingSpanOptions} + */ + constructor( + name: string, + traceId: string, + spanId: string, + tracingContext?: TracingContext, + spanOptions?: TracingSpanOptions + ) { + this.name = name; + this.spanKind = spanOptions?.spanKind; + this.tracingContext = tracingContext; + this.traceId = traceId; + this.spanId = spanId; + } + + spanStatus?: SpanStatus; + attributes: Record = {}; + endCalled = false; + exception?: string | Error; + setStatus(status: SpanStatus): void { + this.spanStatus = status; + } + setAttribute(name: string, value: unknown): void { + this.attributes[name] = value; + } + end(): void { + this.endCalled = true; + } + recordException(exception: string | Error): void { + this.exception = exception; + } + + isRecording(): boolean { + return true; + } + + parentSpan(): MockTracingSpan | undefined { + return this.tracingContext?.getValue(spanKey) as MockTracingSpan; + } +} diff --git a/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts b/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts new file mode 100644 index 000000000000..a97c2b019298 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Simple representation of a Span that only has name and child relationships. + * Children should be arranged in the order they were created. + */ +export interface SpanGraphNode { + /** + * The Span name + */ + name: string; + /** + * All child Spans of this Span + */ + children: SpanGraphNode[]; +} + +/** + * Contains all the spans for a particular TraceID + * starting at unparented roots + */ +export interface SpanGraph { + /** + * All Spans without a parentSpanId + */ + roots: SpanGraphNode[]; +} diff --git a/sdk/test-utils/test-utils/src/tracing/testSpan.ts b/sdk/test-utils/test-utils/src/tracing/testSpan.ts index 30b1230f00e0..363c39b9edef 100644 --- a/sdk/test-utils/test-utils/src/tracing/testSpan.ts +++ b/sdk/test-utils/test-utils/src/tracing/testSpan.ts @@ -2,17 +2,16 @@ // Licensed under the MIT license. import { - TimeInput, - Tracer, + Span, + SpanAttributeValue, + SpanAttributes, + SpanContext, SpanKind, + Tracer, + TimeInput, SpanStatus, - SpanContext, - SpanAttributes, SpanStatusCode, - SpanAttributeValue, - Span, -} from "@azure/core-tracing"; - +} from "@opentelemetry/api"; /** * A mock span useful for testing. */ @@ -78,9 +77,7 @@ export class TestSpan implements Span { this.kind = kind; this.startTime = startTime; this.parentSpanId = parentSpanId; - this.status = { - code: SpanStatusCode.OK, - }; + this.status = { code: SpanStatusCode.OK }; this.endCalled = false; this._context = context; this.attributes = attributes; diff --git a/sdk/test-utils/test-utils/src/tracing/testTracer.ts b/sdk/test-utils/test-utils/src/tracing/testTracer.ts index 8027e122428e..7cc2eaadaf94 100644 --- a/sdk/test-utils/test-utils/src/tracing/testTracer.ts +++ b/sdk/test-utils/test-utils/src/tracing/testTracer.ts @@ -9,35 +9,10 @@ import { TraceFlags, Context as OTContext, context as otContext, - getSpanContext, Tracer, -} from "@azure/core-tracing"; - -/** - * Simple representation of a Span that only has name and child relationships. - * Children should be arranged in the order they were created. - */ -export interface SpanGraphNode { - /** - * The Span name - */ - name: string; - /** - * All child Spans of this Span - */ - children: SpanGraphNode[]; -} - -/** - * Contains all the spans for a particular TraceID - * starting at unparented roots - */ -export interface SpanGraph { - /** - * All Spans without a parentSpanId - */ - roots: SpanGraphNode[]; -} + trace as otTrace, +} from "@opentelemetry/api"; +import { SpanGraph, SpanGraphNode } from "./spanGraphModel"; /** * A mock tracer useful for testing @@ -167,3 +142,39 @@ export class TestTracer implements Tracer { throw new Error("Method not implemented."); } } + +/** + * Get the span context of the span if it exists. + * + * @param context - context to get values from + */ +export function getSpanContext(context: Context): SpanContext | undefined { + return otTrace.getSpanContext(context); +} + +/** + * OpenTelemetry compatible interface for Context + */ +export interface Context { + /** + * Get a value from the context. + * + * @param key - key which identifies a context value + */ + getValue(key: symbol): unknown; + /** + * Create a new context which inherits from this context and has + * the given key set to the given value. + * + * @param key - context key for which to set the value + * @param value - value to set for the given key + */ + setValue(key: symbol, value: unknown): Context; + /** + * Return a new context which inherits from this context but does + * not contain a value for the given key. + * + * @param key - context key for which to clear a value + */ + deleteValue(key: symbol): Context; +} diff --git a/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts b/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts index 4874d8c7aff9..92c6f2d58bf2 100644 --- a/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts +++ b/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts @@ -4,9 +4,25 @@ import { TestTracer } from "./testTracer"; // This must be the same as the default tracer name supplied from @azure/core-tracing. const TRACER_NAME = "azure/core-tracing"; +/** + * Implementation for TracerProvider from opentelemetry/api package. + * It is a registry for creating named tracers. + * This is exported only so that we can support packages using @azure/core-tracing <= 1.0.0-preview.13 + * while transitioning to @azure/core-tracing >= 1.0.0-preview.14 + */ export class TestTracerProvider implements TracerProvider { private tracerCache: Map = new Map(); - + /** + * Returns a Tracer, creating one if one with the given name and version is + * not already created. + * + * This function may return different Tracer types (e.g. + * NoopTracerProvider vs. a functional tracer). + * + * @param name The name of the tracer or instrumentation library. + * @param version The version of the tracer or instrumentation library. + * @returns Tracer A Tracer with the given name and version + */ getTracer(name: string, _version?: string): TestTracer { if (!this.tracerCache.has(name)) { this.tracerCache.set(name, new TestTracer(name, name)); @@ -14,15 +30,21 @@ export class TestTracerProvider implements TracerProvider { return this.tracerCache.get(name)!; } - register() { + /** + * Registers the current tracer provider + */ + register(): void { trace.setGlobalTracerProvider(this); } - disable() { + /** + * Removes global trace provider + */ + disable(): void { trace.disable(); } - setTracer(tracer: TestTracer) { + setTracer(tracer: TestTracer): void { this.tracerCache.set(TRACER_NAME, tracer); } } diff --git a/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts b/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts new file mode 100644 index 000000000000..580bb5f9d770 --- /dev/null +++ b/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createTracingClient, TracingClient, useInstrumenter } from "@azure/core-tracing"; +import { MockTracingSpan, MockInstrumenter } from "../../src"; +import chai, { assert, expect } from "chai"; +import { chaiAzureTrace } from "../../src/tracing/chaiAzureTrace"; +import { MockContext } from "../../src/tracing/mockContext"; +import { OperationTracingOptions } from "@azure/core-tracing"; +chai.use(chaiAzureTrace); + +describe("TestInstrumenter", function () { + let instrumenter: MockInstrumenter; + + beforeEach(function () { + instrumenter = new MockInstrumenter(); + }); + + describe("#startSpan", function () { + it("starts a span and adds to startedSpans array", function () { + const { span } = instrumenter.startSpan("testSpan"); + assert.equal(instrumenter.startedSpans.length, 1); + assert.equal(instrumenter.startedSpans[0], span as MockTracingSpan); + assert.equal(instrumenter.startedSpans[0].name, "testSpan"); + }); + + it("returns a new context with existing attributes", function () { + const existingContext = new MockContext().setValue(Symbol.for("foo"), "bar"); + + const { tracingContext: newContext } = instrumenter.startSpan("testSpan", { + packageName: "test", + tracingContext: existingContext, + }); + + assert.equal(newContext.getValue(Symbol.for("foo")), "bar"); + }); + }); + + describe("#withContext", function () { + it("sets the active context in synchronous functions", async function () { + const { tracingContext } = instrumenter.startSpan("contextTest"); + // TODO: figure out how to be smarter about not wrapping sync functions in promise... + const result = await instrumenter.withContext(tracingContext, function () { + assert.equal(instrumenter.currentContext(), tracingContext); + return 42; + }); + + assert.equal(result, 42); + assert.notEqual(instrumenter.currentContext(), tracingContext); + }); + + it("sets the active context during async functions", async function () { + const { tracingContext } = instrumenter.startSpan("contextTest"); + const result = await instrumenter.withContext(tracingContext, async function () { + await new Promise((resolve) => setTimeout(resolve, 1000)); + assert.equal(instrumenter.currentContext(), tracingContext); + return 42; + }); + assert.equal(result, 42); + assert.notEqual(instrumenter.currentContext(), tracingContext); + }); + + it("resets the previous context after the function returns", async function () { + const existingContext = instrumenter.currentContext(); + const { tracingContext } = instrumenter.startSpan("test"); + await instrumenter.withContext(tracingContext, async function () { + // no-op + }); + assert.equal(instrumenter.currentContext(), existingContext); + }); + }); +}); + +describe("TestInstrumenter with MockClient", function () { + let instrumenter: MockInstrumenter; + let client: MockClientToTest; + + beforeEach(function () { + instrumenter = new MockInstrumenter(); + useInstrumenter(instrumenter); + client = new MockClientToTest(); + }); + + it("starts a span and adds to startedSpans array", async function () { + await client.method(); + assert.equal(instrumenter.startedSpans.length, 1); + assert.equal(instrumenter.startedSpans[0].name, "MockClientToTest.method"); + }); +}); + +describe("Test supportsTracing plugin functionality", function () { + let client: MockClientToTest; + beforeEach(function () { + client = new MockClientToTest(); + }); + + it("supportsTracing with assert", async function () { + await assert.supportsTracing((options) => client.method(options), ["MockClientToTest.method"]); + }); + + it("supportsTracing with expect", async function () { + await expect((options: any) => client.method(options)).to.supportTracing([ + "MockClientToTest.method", + ]); + }); +}); + +/** + * Represent a convenience client that has enabled tracing on a single method. + * Used for testing assertions. + */ +export class MockClientToTest { + public record: Record; + tracingClient: TracingClient; + + constructor() { + this.record = {}; + this.tracingClient = createTracingClient({ + namespace: "Microsoft.Test", + packageName: "@azure/test", + packageVersion: "foobar", + }); + } + + async method(options?: Options) { + return this.tracingClient.withSpan("MockClientToTest.method", options || {}, () => 42, { + spanKind: "consumer", + }); + } +} diff --git a/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts b/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts new file mode 100644 index 000000000000..80c812cd84e8 --- /dev/null +++ b/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { MockTracingSpan } from "../../src"; +import { assert } from "chai"; + +describe("TestTracingSpan", function () { + let subject: MockTracingSpan; + + beforeEach(() => { + subject = new MockTracingSpan("test", "traceId", "spanId"); + }); + + it("records status correctly", function () { + subject.setStatus({ status: "success" }); + assert.deepEqual(subject.spanStatus, { status: "success" }); + }); + + it("records attributes correctly", async function () { + subject.setAttribute("attribute1", "value1"); + subject.setAttribute("attribute2", "value2"); + assert.equal(subject.attributes["attribute1"], "value1"); + assert.equal(subject.attributes["attribute2"], "value2"); + }); + + it("records calls to `end` correctly", function () { + assert.equal(subject.endCalled, false); + subject.end(); + assert.equal(subject.endCalled, true); + }); + + it("records exceptions", function () { + const expectedException = new Error("foo"); + subject.recordException(expectedException); + assert.strictEqual(subject.exception, expectedException); + }); +}); From f167f147bb9af45144322391282e1c6c7a219dee Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Tue, 11 Jan 2022 17:19:13 -0800 Subject: [PATCH 2/7] post-rebase rush update --- common/config/rush/pnpm-lock.yaml | 523 +++++++++++++++++++++++++++++- 1 file changed, 521 insertions(+), 2 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 8601d60a8690..2e9c50a646a6 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -135,6 +135,7 @@ specifiers: '@rush-temp/mock-hub': file:./projects/mock-hub.tgz '@rush-temp/monitor-opentelemetry-exporter': file:./projects/monitor-opentelemetry-exporter.tgz '@rush-temp/monitor-query': file:./projects/monitor-query.tgz + '@rush-temp/opentelemetry-instrumentation-azure-sdk': file:./projects/opentelemetry-instrumentation-azure-sdk.tgz '@rush-temp/perf-ai-form-recognizer': file:./projects/perf-ai-form-recognizer.tgz '@rush-temp/perf-ai-metrics-advisor': file:./projects/perf-ai-metrics-advisor.tgz '@rush-temp/perf-ai-text-analytics': file:./projects/perf-ai-text-analytics.tgz @@ -322,6 +323,7 @@ dependencies: '@rush-temp/mock-hub': file:projects/mock-hub.tgz '@rush-temp/monitor-opentelemetry-exporter': file:projects/monitor-opentelemetry-exporter.tgz '@rush-temp/monitor-query': file:projects/monitor-query.tgz + '@rush-temp/opentelemetry-instrumentation-azure-sdk': file:projects/opentelemetry-instrumentation-azure-sdk.tgz '@rush-temp/perf-ai-form-recognizer': file:projects/perf-ai-form-recognizer.tgz '@rush-temp/perf-ai-metrics-advisor': file:projects/perf-ai-metrics-advisor.tgz '@rush-temp/perf-ai-text-analytics': file:projects/perf-ai-text-analytics.tgz @@ -1360,6 +1362,11 @@ packages: '@opentelemetry/api': 1.0.3 dev: false + /@opentelemetry/api-metrics/0.27.0: + resolution: {integrity: sha512-tB79288bwjkdhPNpw4UdOEy3bacVwtol6Que7cAu8KEJ9ULjRfSiwpYEwJY/oER3xZ7zNFz0uiJ7N1jSiotpVA==} + engines: {node: '>=8.0.0'} + dev: false + /@opentelemetry/api/0.10.2: resolution: {integrity: sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==} engines: {node: '>=8.0.0'} @@ -1402,6 +1409,16 @@ packages: semver: 7.3.5 dev: false + /@opentelemetry/core/1.0.1_@opentelemetry+api@1.0.3: + resolution: {integrity: sha512-90nQ2X6b/8X+xjcLDBYKooAcOsIlwLRYm+1VsxcX5cHl6V4CSVmDpBreQSDH/A21SqROzapk6813008SatmPpQ==} + engines: {node: '>=8.5.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.1.0' + dependencies: + '@opentelemetry/api': 1.0.3 + '@opentelemetry/semantic-conventions': 1.0.1 + dev: false + /@opentelemetry/instrumentation-http/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-vqM1hqgYtcO8Upq8pl4I+YW0bnodHlUSSKYuOH7m9Aujbi571pU3zFctpiU5pNhj9eLEJ/r7aOTV6O4hCxqOjQ==} engines: {node: '>=8.0.0'} @@ -1431,6 +1448,20 @@ packages: - supports-color dev: false + /@opentelemetry/instrumentation/0.27.0_@opentelemetry+api@1.0.3: + resolution: {integrity: sha512-dUwY/VoDptdK8AYigwS3IKblG+unV5xIdV4VQKy+nX5aT3f7vd5PMYs4arCQSYLbLRe0s7GxK6S9dtjai/TsHQ==} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.0.3 + '@opentelemetry/api-metrics': 0.27.0 + require-in-the-middle: 5.1.0 + semver: 7.3.5 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + /@opentelemetry/node/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-+HhGbDruQ7cwejVOIYyxRa28uosnG8W95NiQZ6qE8PXXPsDSyGeftAPbtYpGit0H2f5hrVcMlwmWHeAo9xkSLA==} engines: {node: '>=8.0.0'} @@ -1488,6 +1519,11 @@ packages: engines: {node: '>=8.0.0'} dev: false + /@opentelemetry/semantic-conventions/1.0.1: + resolution: {integrity: sha512-7XU1sfQ8uCVcXLxtAHA8r3qaLJ2oq7sKtEwzZhzuEXqYmjW+n+J4yM3kNo0HQo3Xp1eUe47UM6Wy6yuAvIyllg==} + engines: {node: '>=8.0.0'} + dev: false + /@opentelemetry/tracing/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-EFrKTFndiEdh/KnzwDgo/EcphG/5z/NyLck8oiUUY+YMP7hskXNYHjTWSAv9UxtYe1MzgLbjmAateTuMmFIVNw==} engines: {node: '>=8.0.0'} @@ -1625,6 +1661,18 @@ packages: '@sinonjs/commons': 1.8.3 dev: false + /@sinonjs/fake-timers/7.1.2: + resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==} + dependencies: + '@sinonjs/commons': 1.8.3 + dev: false + + /@sinonjs/fake-timers/8.1.0: + resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} + dependencies: + '@sinonjs/commons': 1.8.3 + dev: false + /@sinonjs/samsam/5.3.1: resolution: {integrity: sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==} dependencies: @@ -1633,6 +1681,14 @@ packages: type-detect: 4.0.8 dev: false + /@sinonjs/samsam/6.0.2: + resolution: {integrity: sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==} + dependencies: + '@sinonjs/commons': 1.8.3 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: false + /@sinonjs/text-encoding/0.7.1: resolution: {integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==} dev: false @@ -1809,6 +1865,10 @@ packages: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: false + /@types/minimatch/3.0.3: + resolution: {integrity: sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==} + dev: false + /@types/minimatch/3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} dev: false @@ -1893,6 +1953,12 @@ packages: '@types/node': 17.0.1 dev: false + /@types/sinon/10.0.6: + resolution: {integrity: sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==} + dependencies: + '@sinonjs/fake-timers': 7.1.2 + dev: false + /@types/sinon/9.0.11: resolution: {integrity: sha512-PwP4UY33SeeVKodNE37ZlOsR9cReypbMJOhZ7BVE0lB+Hix3efCOxiJWiE5Ia+yL9Cn2Ch72EjFTRze8RZsNtg==} dependencies: @@ -2197,6 +2263,13 @@ packages: picomatch: 2.3.0 dev: false + /append-transform/1.0.0: + resolution: {integrity: sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==} + engines: {node: '>=4'} + dependencies: + default-require-extensions: 2.0.0 + dev: false + /append-transform/2.0.0: resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} engines: {node: '>=8'} @@ -2395,6 +2468,12 @@ packages: regenerator-runtime: 0.11.1 dev: false + /backbone/1.4.0: + resolution: {integrity: sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==} + dependencies: + underscore: 1.13.2 + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: false @@ -2513,6 +2592,16 @@ packages: engines: {node: '>= 0.8'} dev: false + /caching-transform/3.0.2: + resolution: {integrity: sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==} + engines: {node: '>=6'} + dependencies: + hasha: 3.0.0 + make-dir: 2.1.0 + package-hash: 3.0.0 + write-file-atomic: 2.4.3 + dev: false + /caching-transform/4.0.0: resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} engines: {node: '>=8'} @@ -2840,6 +2929,17 @@ packages: vary: 1.1.2 dev: false + /cp-file/6.2.0: + resolution: {integrity: sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.8 + make-dir: 2.1.0 + nested-error-stacks: 2.1.0 + pify: 4.0.1 + safe-buffer: 5.2.1 + dev: false + /create-require/1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: false @@ -2852,6 +2952,13 @@ packages: cross-spawn: 7.0.3 dev: false + /cross-spawn/4.0.2: + resolution: {integrity: sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=} + dependencies: + lru-cache: 4.1.5 + which: 1.3.1 + dev: false + /cross-spawn/6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -2999,6 +3106,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /default-require-extensions/2.0.0: + resolution: {integrity: sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=} + engines: {node: '>=4'} + dependencies: + strip-bom: 3.0.0 + dev: false + /default-require-extensions/3.0.0: resolution: {integrity: sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==} engines: {node: '>=8'} @@ -3065,6 +3179,11 @@ packages: engines: {node: '>=0.3.1'} dev: false + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: false + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3113,6 +3232,14 @@ packages: engines: {node: '>=10'} dev: false + /downlevel-dts/0.4.0: + resolution: {integrity: sha512-nh5vM3n2pRhPwZqh0iWo5gpItPAYEGEWw9yd0YpI+lO60B7A3A6iJlxDbt7kKVNbqBXKsptL+jwE/Yg5Go66WQ==} + hasBin: true + dependencies: + shelljs: 0.8.4 + typescript: 3.9.10 + dev: false + /downlevel-dts/0.8.0: resolution: {integrity: sha512-wBy+Q0Ya/1XRz9MMaj3BXH95E8aSckY3lppmUnf8Qv7dUg0wbWm3szDiVL4PdAvwcS7JbBBDPhCXeAGNT3ttFQ==} hasBin: true @@ -3712,6 +3839,15 @@ packages: unpipe: 1.0.0 dev: false + /find-cache-dir/2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + dev: false + /find-cache-dir/3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -3796,6 +3932,13 @@ packages: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} dev: false + /foreground-child/1.5.6: + resolution: {integrity: sha1-T9ca0t/elnibmApcCilZN8svXOk=} + dependencies: + cross-spawn: 4.0.2 + signal-exit: 3.0.6 + dev: false + /foreground-child/2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -4067,6 +4210,19 @@ packages: resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} dev: false + /handlebars/4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.5 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.14.5 + dev: false + /has-ansi/2.0.0: resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=} engines: {node: '>=0.10.0'} @@ -4123,6 +4279,13 @@ packages: function-bind: 1.1.1 dev: false + /hasha/3.0.0: + resolution: {integrity: sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=} + engines: {node: '>=4'} + dependencies: + is-stream: 1.1.0 + dev: false + /hasha/5.2.2: resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} engines: {node: '>=8'} @@ -4136,6 +4299,12 @@ packages: hasBin: true dev: false + /highlight.js/9.18.5: + resolution: {integrity: sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==} + deprecated: Support has ended for 9.x series. Upgrade to @latest + requiresBuild: true + dev: false + /homedir-polyfill/1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -4453,6 +4622,11 @@ packages: resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==} dev: false + /is-stream/1.1.0: + resolution: {integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=} + engines: {node: '>=0.10.0'} + dev: false + /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -4536,11 +4710,23 @@ packages: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} dev: false + /istanbul-lib-coverage/2.0.5: + resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} + engines: {node: '>=6'} + dev: false + /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} dev: false + /istanbul-lib-hook/2.0.7: + resolution: {integrity: sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==} + engines: {node: '>=6'} + dependencies: + append-transform: 1.0.0 + dev: false + /istanbul-lib-hook/3.0.0: resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} engines: {node: '>=8'} @@ -4548,6 +4734,21 @@ packages: append-transform: 2.0.0 dev: false + /istanbul-lib-instrument/3.3.0: + resolution: {integrity: sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==} + engines: {node: '>=6'} + dependencies: + '@babel/generator': 7.16.5 + '@babel/parser': 7.16.6 + '@babel/template': 7.16.0 + '@babel/traverse': 7.16.5 + '@babel/types': 7.16.0 + istanbul-lib-coverage: 2.0.5 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /istanbul-lib-instrument/4.0.3: resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} engines: {node: '>=8'} @@ -4573,6 +4774,15 @@ packages: uuid: 3.4.0 dev: false + /istanbul-lib-report/2.0.8: + resolution: {integrity: sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==} + engines: {node: '>=6'} + dependencies: + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + supports-color: 6.1.0 + dev: false + /istanbul-lib-report/3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} engines: {node: '>=8'} @@ -4582,6 +4792,19 @@ packages: supports-color: 7.2.0 dev: false + /istanbul-lib-source-maps/3.0.6: + resolution: {integrity: sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==} + engines: {node: '>=6'} + dependencies: + debug: 4.3.3 + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + rimraf: 2.7.1 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: false + /istanbul-lib-source-maps/4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} @@ -4593,6 +4816,13 @@ packages: - supports-color dev: false + /istanbul-reports/2.2.7: + resolution: {integrity: sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==} + engines: {node: '>=6'} + dependencies: + html-escaper: 2.0.2 + dev: false + /istanbul-reports/3.1.1: resolution: {integrity: sha512-q1kvhAXWSsXfMjCdNHNPKZZv94OlspKnoGv+R9RGbnqOOQ0VbNfLFgQDVgi7hHenKsndGq3/o0OBdzDXthWcNw==} engines: {node: '>=8'} @@ -4618,6 +4848,10 @@ packages: resolution: {integrity: sha1-o6vicYryQaKykE+EpiWXDzia4yo=} dev: false + /jquery/3.6.0: + resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} + dev: false + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false @@ -5090,6 +5324,13 @@ packages: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} dev: false + /lru-cache/4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: false + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -5097,6 +5338,10 @@ packages: yallist: 4.0.0 dev: false + /lunr/2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: false + /machina/4.0.2: resolution: {integrity: sha512-OOlFrW1rd783S6tF36v5Ie/TM64gfvSl9kYLWL2cPA31J71HHWW3XrgSe1BZSFAPkh8532CMJMLv/s9L2aopiA==} engines: {node: '>=0.4.0'} @@ -5110,6 +5355,14 @@ packages: sourcemap-codec: 1.4.8 dev: false + /make-dir/2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + dependencies: + pify: 4.0.1 + semver: 5.7.1 + dev: false + /make-dir/3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -5121,6 +5374,12 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: false + /marked/0.7.0: + resolution: {integrity: sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==} + engines: {node: '>=0.10.0'} + hasBin: true + dev: false + /matched/1.0.2: resolution: {integrity: sha512-7ivM1jFZVTOOS77QsR+TtYHH0ecdLclMkqbf5qiJdX2RorqfhsL65QHySPZgDE0ZjHoh+mQUNHTanNXIlzXd0Q==} engines: {node: '>= 0.12.0'} @@ -5155,6 +5414,12 @@ packages: resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} dev: false + /merge-source-map/1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + dependencies: + source-map: 0.6.1 + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -5249,6 +5514,19 @@ packages: hasBin: true dev: false + /mocha-junit-reporter/1.23.3_mocha@7.2.0: + resolution: {integrity: sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA==} + peerDependencies: + mocha: '>=2.2.5' + dependencies: + debug: 2.6.9 + md5: 2.3.0 + mkdirp: 0.5.5 + mocha: 7.2.0 + strip-ansi: 4.0.0 + xml: 1.0.1 + dev: false + /mocha-junit-reporter/2.0.2_mocha@7.2.0: resolution: {integrity: sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg==} peerDependencies: @@ -5360,6 +5638,14 @@ packages: engines: {node: '>= 0.6'} dev: false + /neo-async/2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /nested-error-stacks/2.1.0: + resolution: {integrity: sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==} + dev: false + /nice-try/1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: false @@ -5374,6 +5660,16 @@ packages: path-to-regexp: 1.8.0 dev: false + /nise/5.1.0: + resolution: {integrity: sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==} + dependencies: + '@sinonjs/commons': 1.8.3 + '@sinonjs/fake-timers': 7.1.2 + '@sinonjs/text-encoding': 0.7.1 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: false + /nock/12.0.3: resolution: {integrity: sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==} engines: {node: '>= 10.13'} @@ -5495,6 +5791,40 @@ packages: engines: {node: '>=0.10.0'} dev: false + /nyc/14.1.1: + resolution: {integrity: sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==} + engines: {node: '>=6'} + hasBin: true + dependencies: + archy: 1.0.0 + caching-transform: 3.0.2 + convert-source-map: 1.8.0 + cp-file: 6.2.0 + find-cache-dir: 2.1.0 + find-up: 3.0.0 + foreground-child: 1.5.6 + glob: 7.2.0 + istanbul-lib-coverage: 2.0.5 + istanbul-lib-hook: 2.0.7 + istanbul-lib-instrument: 3.3.0 + istanbul-lib-report: 2.0.8 + istanbul-lib-source-maps: 3.0.6 + istanbul-reports: 2.2.7 + js-yaml: 3.14.1 + make-dir: 2.1.0 + merge-source-map: 1.1.0 + resolve-from: 4.0.0 + rimraf: 2.7.1 + signal-exit: 3.0.6 + spawn-wrap: 1.4.3 + test-exclude: 5.2.3 + uuid: 3.4.0 + yargs: 13.3.2 + yargs-parser: 13.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /nyc/15.1.0: resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==} engines: {node: '>=8.9'} @@ -5639,6 +5969,11 @@ packages: word-wrap: 1.2.3 dev: false + /os-homedir/1.0.2: + resolution: {integrity: sha1-/7xJiDNuDoM94MFox+8VISGqf7M=} + engines: {node: '>=0.10.0'} + dev: false + /p-limit/1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -5691,6 +6026,16 @@ packages: engines: {node: '>=6'} dev: false + /package-hash/3.0.0: + resolution: {integrity: sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.8 + hasha: 3.0.0 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + dev: false + /package-hash/4.0.0: resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} engines: {node: '>=8'} @@ -5814,6 +6159,11 @@ packages: engines: {node: '>=4'} dev: false + /pify/4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: false + /pkg-dir/2.0.0: resolution: {integrity: sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=} engines: {node: '>=4'} @@ -5821,6 +6171,13 @@ packages: find-up: 2.1.0 dev: false + /pkg-dir/3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + dev: false + /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -5923,6 +6280,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /pseudomap/1.0.2: + resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} + dev: false + /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: false @@ -6038,6 +6399,14 @@ packages: strip-json-comments: 2.0.1 dev: false + /read-pkg-up/4.0.0: + resolution: {integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + read-pkg: 3.0.0 + dev: false + /read-pkg/3.0.0: resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=} engines: {node: '>=4'} @@ -6219,6 +6588,13 @@ packages: debug: 3.2.7 dev: false + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: false + /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -6466,6 +6842,17 @@ packages: simple-concat: 1.0.1 dev: false + /sinon/12.0.1: + resolution: {integrity: sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==} + dependencies: + '@sinonjs/commons': 1.8.3 + '@sinonjs/fake-timers': 8.1.0 + '@sinonjs/samsam': 6.0.2 + diff: 5.0.0 + nise: 5.1.0 + supports-color: 7.2.0 + dev: false + /sinon/9.2.4: resolution: {integrity: sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==} dependencies: @@ -6622,6 +7009,17 @@ packages: resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=} dev: false + /spawn-wrap/1.4.3: + resolution: {integrity: sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==} + dependencies: + foreground-child: 1.5.6 + mkdirp: 0.5.5 + os-homedir: 1.0.2 + rimraf: 2.7.1 + signal-exit: 3.0.6 + which: 1.3.1 + dev: false + /spawn-wrap/2.0.0: resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} engines: {node: '>=8'} @@ -6908,6 +7306,16 @@ packages: source-map-support: 0.5.21 dev: false + /test-exclude/5.2.3: + resolution: {integrity: sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==} + engines: {node: '>=6'} + dependencies: + glob: 7.2.0 + minimatch: 3.0.4 + read-pkg-up: 4.0.0 + require-main-filename: 2.0.0 + dev: false + /test-exclude/6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -7151,6 +7559,46 @@ packages: is-typedarray: 1.0.0 dev: false + /typedoc-default-themes/0.6.3: + resolution: {integrity: sha512-rouf0TcIA4M2nOQFfC7Zp4NEwoYiEX4vX/ZtudJWU9IHA29MPC+PPgSXYLPESkUo7FuB//GxigO3mk9Qe1xp3Q==} + engines: {node: '>= 8'} + dependencies: + backbone: 1.4.0 + jquery: 3.6.0 + lunr: 2.3.9 + underscore: 1.13.2 + dev: false + + /typedoc/0.15.2: + resolution: {integrity: sha512-K2nFEtyDQTVdXOzYtECw3TwuT3lM91Zc0dzGSLuor5R8qzZbwqBoCw7xYGVBow6+mEZAvKGznLFsl7FzG+wAgQ==} + engines: {node: '>= 6.0.0'} + hasBin: true + dependencies: + '@types/minimatch': 3.0.3 + fs-extra: 8.1.0 + handlebars: 4.7.7 + highlight.js: 9.18.5 + lodash: 4.17.21 + marked: 0.7.0 + minimatch: 3.0.4 + progress: 2.0.3 + shelljs: 0.8.4 + typedoc-default-themes: 0.6.3 + typescript: 3.7.7 + dev: false + + /typescript/3.7.7: + resolution: {integrity: sha512-MmQdgo/XenfZPvVLtKZOq9jQQvzaUAUpcKW8Z43x9B2fOm4S5g//tPtMweZUIP+SoBqrVPEIm+dJeQ9dfO0QdA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /typescript/3.9.10: + resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + /typescript/4.2.4: resolution: {integrity: sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==} engines: {node: '>=4.2.0'} @@ -7195,6 +7643,10 @@ packages: through: 2.3.8 dev: false + /underscore/1.13.2: + resolution: {integrity: sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==} + dev: false + /universal-user-agent/6.0.0: resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==} dev: false @@ -7372,6 +7824,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /wordwrap/1.0.0: + resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=} + dev: false + /wrap-ansi/5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -7403,6 +7859,14 @@ packages: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} dev: false + /write-file-atomic/2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + dependencies: + graceful-fs: 4.2.8 + imurmurhash: 0.1.4 + signal-exit: 3.0.6 + dev: false + /write-file-atomic/3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} dependencies: @@ -7495,6 +7959,10 @@ packages: engines: {node: '>=10'} dev: false + /yallist/2.1.2: + resolution: {integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=} + dev: false + /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false @@ -10621,7 +11089,7 @@ packages: dev: false file:projects/core-tracing.tgz: - resolution: {integrity: sha512-0an4uRa/vsk2n5k4n7uCz2ElXpveIyRVQFmXaaGC0eE02K1m4ts0JBD3CJEWsGT4hWzjed+ph+vCoyvRZS5irw==, tarball: file:projects/core-tracing.tgz} + resolution: {integrity: sha512-jbrBdV2RkHhskzce4H2Mr5DlutfswJErgAZvLAvlswMGwVF6toTtCGZcAaxFQpZzcOkxWVTEyma42v65lPDN6A==, tarball: file:projects/core-tracing.tgz} name: '@rush-temp/core-tracing' version: 0.0.0 dependencies: @@ -11893,6 +12361,57 @@ packages: - utf-8-validate dev: false + file:projects/opentelemetry-instrumentation-azure-sdk.tgz: + resolution: {integrity: sha512-Dz8EkDVTh8SfKUFatcu/WZUX6YQjIQaE3A1qmM9RPSGXloRh/3TmyrrqqKvP6WRoyYIosCqRbwvzMUdyPljP/g==, tarball: file:projects/opentelemetry-instrumentation-azure-sdk.tgz} + name: '@rush-temp/opentelemetry-instrumentation-azure-sdk' + version: 0.0.0 + dependencies: + '@microsoft/api-extractor': 7.19.2 + '@opentelemetry/api': 1.0.3 + '@opentelemetry/core': 1.0.1_@opentelemetry+api@1.0.3 + '@opentelemetry/instrumentation': 0.27.0_@opentelemetry+api@1.0.3 + '@types/chai': 4.3.0 + '@types/mocha': 7.0.2 + '@types/node': 12.20.40 + '@types/sinon': 10.0.6 + chai: 4.3.4 + cross-env: 7.0.3 + dotenv: 8.6.0 + downlevel-dts: 0.4.0 + eslint: 7.32.0 + esm: 3.2.25 + inherits: 2.0.4 + karma: 6.3.9 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.1.0 + karma-edge-launcher: 0.4.2_karma@6.3.9 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@6.3.9 + karma-json-preprocessor: 0.3.3_karma@6.3.9 + karma-json-to-file-reporter: 1.0.1 + karma-junit-reporter: 2.0.1_karma@6.3.9 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@6.3.9 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + nyc: 14.1.1 + prettier: 2.5.1 + rimraf: 3.0.2 + rollup: 1.32.1 + sinon: 12.0.1 + source-map-support: 0.5.21 + tslib: 2.3.1 + typedoc: 0.15.2 + typescript: 4.2.4 + util: 0.12.4 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + file:projects/perf-ai-form-recognizer.tgz: resolution: {integrity: sha512-0VjfzGd47tkR/d9bIMi7fhm4V421vHWjp9AHAzIfMRT7rey5JsFzdpeJObqUc6jnA4n+xXq8HRgqVnibfzQXfw==, tarball: file:projects/perf-ai-form-recognizer.tgz} name: '@rush-temp/perf-ai-form-recognizer' @@ -13603,7 +14122,7 @@ packages: dev: false file:projects/test-utils.tgz: - resolution: {integrity: sha512-lRA7wxoXX7NWHc36e1+U8RH2RQmoZlFm50QXbLBRXqr+A8eIi8u5lphwOZchx6UzOpv1AatfESPoxcp5n7iyzA==, tarball: file:projects/test-utils.tgz} + resolution: {integrity: sha512-CEWMLlxQTpRRiRBCJi1fRyGOlLMmxCgd4C/5JwIekQL4LtpQ6OvRVGyJFSpJ4Y6dYxAwXyYnEF9MWrDWURAtIQ==, tarball: file:projects/test-utils.tgz} name: '@rush-temp/test-utils' version: 0.0.0 dependencies: From 419099bc04b74f6908c3134cb519aaaaa93e9c3b Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Tue, 11 Jan 2022 17:25:16 -0800 Subject: [PATCH 3/7] Named types for span status union --- .../core-tracing/review/core-tracing.api.md | 12 +++++++++--- sdk/core/core-tracing/src/index.ts | 2 ++ sdk/core/core-tracing/src/interfaces.ts | 19 +++++++++++-------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 4b3b02b881a0..d4f53be814e2 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -31,13 +31,19 @@ export interface OperationTracingOptions { } // @public -export type SpanStatus = { - status: "success"; -} | { +export type SpanStatus = SpanStatusSuccess | SpanStatusError; + +// @public +export type SpanStatusError = { status: "error"; error?: Error | string; }; +// @public +export type SpanStatusSuccess = { + status: "success"; +}; + // @public export interface TracingClient { createRequestHeaders(tracingContext?: TracingContext): Record; diff --git a/sdk/core/core-tracing/src/index.ts b/sdk/core/core-tracing/src/index.ts index 082ca88bf334..423149e112f8 100644 --- a/sdk/core/core-tracing/src/index.ts +++ b/sdk/core/core-tracing/src/index.ts @@ -6,6 +6,8 @@ export { InstrumenterSpanOptions, OperationTracingOptions, SpanStatus, + SpanStatusError, + SpanStatusSuccess, TracingClient, TracingClientOptions, TracingContext, diff --git a/sdk/core/core-tracing/src/interfaces.ts b/sdk/core/core-tracing/src/interfaces.ts index b0e09fe3228e..acfec30a582d 100644 --- a/sdk/core/core-tracing/src/interfaces.ts +++ b/sdk/core/core-tracing/src/interfaces.ts @@ -176,19 +176,22 @@ export interface InstrumenterSpanOptions extends TracingSpanOptions { tracingContext?: TracingContext; } +/** + * Status representing a successful operation that can be sent to {@link TracingSpan.setStatus} + */ +export type SpanStatusSuccess = { status: "success" }; + +/** + * Status representing an error that can be sent to {@link TracingSpan.setStatus} + */ +export type SpanStatusError = { status: "error"; error?: Error | string }; + /** * Represents the statuses that can be passed to {@link TracingSpan.setStatus}. * * By default, all spans will be created with status "unset". */ -export type SpanStatus = - | { - status: "success"; - } - | { - status: "error"; - error?: Error | string; - }; +export type SpanStatus = SpanStatusSuccess | SpanStatusError; /** * Represents an implementation agnostic tracing span. From 4fa61ed9a0e86905120c828e1cd5b5f61bf476d4 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Fri, 7 Jan 2022 07:00:34 -0800 Subject: [PATCH 4/7] wip --- sdk/core/core-http/package.json | 2 +- sdk/core/core-http/review/core-http.api.md | 19 +- sdk/core/core-http/src/createSpanLegacy.ts | 12 +- .../core-http/src/policies/tracingPolicy.ts | 74 +- sdk/core/core-http/src/webResource.ts | 23 +- .../test/policies/tracingPolicyTests.ts | 866 +++--- sdk/eventhub/event-hubs/package.json | 2 +- .../event-hubs/review/event-hubs.api.md | 8 +- .../src/diagnostics/instrumentEventData.ts | 26 +- .../event-hubs/src/diagnostics/tracing.ts | 98 +- sdk/eventhub/event-hubs/src/eventDataBatch.ts | 13 +- .../event-hubs/src/eventHubProducerClient.ts | 22 +- .../event-hubs/src/managementClient.ts | 15 +- .../event-hubs/src/partitionProcessor.ts | 11 +- sdk/eventhub/event-hubs/src/partitionPump.ts | 31 +- .../event-hubs/test/internal/misc.spec.ts | 955 ++++--- .../test/internal/partitionPump.spec.ts | 330 +-- .../event-hubs/test/internal/sender.spec.ts | 2538 ++++++++--------- .../event-hubs/test/public/hubruntime.spec.ts | 564 ++-- .../src/instrumenter.ts | 12 + .../keyvault-common/src/tracingHelpers.ts | 55 +- .../test/utils/supportsTracing.ts | 41 +- sdk/keyvault/keyvault-secrets/package.json | 2 +- sdk/keyvault/keyvault-secrets/src/index.ts | 82 +- .../src/lro/delete/operation.ts | 14 +- .../src/lro/recover/operation.ts | 12 +- .../keyvault-secrets/test/public/CRUD.spec.ts | 9 +- 27 files changed, 2878 insertions(+), 2958 deletions(-) diff --git a/sdk/core/core-http/package.json b/sdk/core/core-http/package.json index 27791f771830..fafd1a35693a 100644 --- a/sdk/core/core-http/package.json +++ b/sdk/core/core-http/package.json @@ -115,7 +115,7 @@ "@azure/abort-controller": "^1.0.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", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", diff --git a/sdk/core/core-http/review/core-http.api.md b/sdk/core/core-http/review/core-http.api.md index 3c58a4b3c93f..a5cdf950b168 100644 --- a/sdk/core/core-http/review/core-http.api.md +++ b/sdk/core/core-http/review/core-http.api.md @@ -8,14 +8,13 @@ import { AbortSignalLike } from '@azure/abort-controller'; import { AccessToken } from '@azure/core-auth'; -import { Context } from '@azure/core-tracing'; import { Debugger } from '@azure/logger'; import { GetTokenOptions } from '@azure/core-auth'; import { isTokenCredential } from '@azure/core-auth'; import { OperationTracingOptions } from '@azure/core-tracing'; -import { Span } from '@azure/core-tracing'; -import { SpanOptions } from '@azure/core-tracing'; import { TokenCredential } from '@azure/core-auth'; +import { TracingContext } from '@azure/core-tracing'; +import { TracingSpan } from '@azure/core-tracing'; export { AbortSignalLike } @@ -168,8 +167,8 @@ export const Constants: { export function createPipelineFromOptions(pipelineOptions: InternalPipelineOptions, authPolicyFactory?: RequestPolicyFactory): ServiceClientOptions; // @public @deprecated -export function createSpanFunction(args: SpanConfig): (operationName: string, operationOptions: T) => { - span: Span; +export function createSpanFunction(_args: SpanConfig): (operationName: string, operationOptions: T) => { + span: TracingSpan; updatedOptions: T; }; @@ -582,7 +581,7 @@ export interface RequestOptionsBase { serializerOptions?: SerializerOptions; shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean); timeout?: number; - tracingContext?: Context; + tracingContext?: TracingContext; } // @public @@ -637,8 +636,7 @@ export interface RequestPrepareOptions { [key: string]: any | ParameterValue; }; serializationMapper?: Mapper; - spanOptions?: SpanOptions; - tracingContext?: Context; + tracingContext?: TracingContext; url?: string; } @@ -871,12 +869,11 @@ export class WebResource implements WebResourceLike { }; requestId: string; shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean); - spanOptions?: SpanOptions; // @deprecated streamResponseBody?: boolean; streamResponseStatusCodes?: Set; timeout: number; - tracingContext?: Context; + tracingContext?: TracingContext; url: string; validateRequestProperties(): void; withCredentials: boolean; @@ -907,7 +904,7 @@ export interface WebResourceLike { streamResponseBody?: boolean; streamResponseStatusCodes?: Set; timeout: number; - tracingContext?: Context; + tracingContext?: TracingContext; url: string; validateRequestProperties(): void; withCredentials: boolean; diff --git a/sdk/core/core-http/src/createSpanLegacy.ts b/sdk/core/core-http/src/createSpanLegacy.ts index 23eebc647d6f..491aae11dbb1 100644 --- a/sdk/core/core-http/src/createSpanLegacy.ts +++ b/sdk/core/core-http/src/createSpanLegacy.ts @@ -5,7 +5,7 @@ // were a part of the GA'd library and can't be removed until the next major // release. They currently get called always, even if tracing is not enabled. -import { Span, createSpanFunction as coreTracingCreateSpanFunction } from "@azure/core-tracing"; +import { createTracingClient, TracingSpan } from "@azure/core-tracing"; import { OperationOptions } from "./operationOptions"; /** @@ -35,10 +35,14 @@ export interface SpanConfig { * @param tracingOptions - The options for the underlying http request. */ export function createSpanFunction( - args: SpanConfig + _args: SpanConfig ): ( operationName: string, operationOptions: T -) => { span: Span; updatedOptions: T } { - return coreTracingCreateSpanFunction(args); +) => { span: TracingSpan; updatedOptions: T } { + return createTracingClient({ + namespace: "Microsoft.CoreHttp", + packageName: "@azure/core-http", + packageVersion: "foo", + }).startSpan; } diff --git a/sdk/core/core-http/src/policies/tracingPolicy.ts b/sdk/core/core-http/src/policies/tracingPolicy.ts index 2d359cc5bc8d..b9f18fec251c 100644 --- a/sdk/core/core-http/src/policies/tracingPolicy.ts +++ b/sdk/core/core-http/src/policies/tracingPolicy.ts @@ -8,23 +8,16 @@ import { RequestPolicyOptions, } from "./requestPolicy"; import { - Span, - SpanKind, - SpanStatusCode, - createSpanFunction, - getTraceParentHeader, - isSpanContextValid, + createTracingClient, + TracingClient, + TracingContext, + TracingSpan, } from "@azure/core-tracing"; import { HttpOperationResponse } from "../httpOperationResponse"; import { URLBuilder } from "../url"; import { WebResourceLike } from "../webResource"; import { logger } from "../log"; -const createSpan = createSpanFunction({ - packagePrefix: "", - namespace: "", -}); - /** * Options to customize the tracing policy. */ @@ -53,6 +46,7 @@ export function tracingPolicy(tracingOptions: TracingPolicyOptions = {}): Reques */ export class TracingPolicy extends BaseRequestPolicy { private userAgent?: string; + private tracingClient: TracingClient; constructor( nextPolicy: RequestPolicy, @@ -61,6 +55,11 @@ export class TracingPolicy extends BaseRequestPolicy { ) { super(nextPolicy, options); this.userAgent = tracingOptions.userAgent; + this.tracingClient = createTracingClient({ + namespace: "Microsoft.CoreHttp", + packageName: "@azure/core-http", + packageVersion: "foo", + }); } public async sendRequest(request: WebResourceLike): Promise { @@ -68,14 +67,16 @@ export class TracingPolicy extends BaseRequestPolicy { return this._nextPolicy.sendRequest(request); } - const span = this.tryCreateSpan(request); + const { span, context } = this.tryCreateSpan(request); if (!span) { return this._nextPolicy.sendRequest(request); } try { - const response = await this._nextPolicy.sendRequest(request); + const response = await this.tracingClient.withContext(context!, () => + this._nextPolicy.sendRequest(request) + ); this.tryProcessResponse(span, response); return response; } catch (err) { @@ -84,17 +85,20 @@ export class TracingPolicy extends BaseRequestPolicy { } } - tryCreateSpan(request: WebResourceLike): Span | undefined { + tryCreateSpan(request: WebResourceLike): { + span: TracingSpan | undefined; + context: TracingContext | undefined; + } { try { const path = URLBuilder.parse(request.url).getPath() || "/"; // Passing spanOptions as part of tracingOptions to maintain compatibility @azure/core-tracing@preview.13 and earlier. // We can pass this as a separate parameter once we upgrade to the latest core-tracing. - const { span } = createSpan(path, { + const { span, updatedOptions } = this.tracingClient.startSpan(path, { tracingOptions: { spanOptions: { ...(request as any).spanOptions, - kind: SpanKind.CLIENT, + spanKind: "client", }, tracingContext: request.tracingContext, }, @@ -103,7 +107,7 @@ export class TracingPolicy extends BaseRequestPolicy { // If the span is not recording, don't do any more work. if (!span.isRecording()) { span.end(); - return undefined; + return { context: updatedOptions.tracingOptions.tracingContext, span: undefined }; } const namespaceFromContext = request.tracingContext?.getValue(Symbol.for("az.namespace")); @@ -112,39 +116,33 @@ export class TracingPolicy extends BaseRequestPolicy { span.setAttribute("az.namespace", namespaceFromContext); } - span.setAttributes({ - "http.method": request.method, - "http.url": request.url, - requestId: request.requestId, - }); + span.setAttribute("http.method", request.method); + span.setAttribute("http.url", request.url); + span.setAttribute("requestId", request.requestId); if (this.userAgent) { span.setAttribute("http.user_agent", this.userAgent); } // set headers - const spanContext = span.spanContext(); - const traceParentHeader = getTraceParentHeader(spanContext); - if (traceParentHeader && isSpanContextValid(spanContext)) { - request.headers.set("traceparent", traceParentHeader); - const traceState = spanContext.traceState && spanContext.traceState.serialize(); - // if tracestate is set, traceparent MUST be set, so only set tracestate after traceparent - if (traceState) { - request.headers.set("tracestate", traceState); - } + const headers = this.tracingClient.createRequestHeaders( + updatedOptions.tracingOptions?.tracingContext + ); + for (const header in headers) { + request.headers.set(header, headers[header]); } - return span; + return { span, context: updatedOptions.tracingOptions.tracingContext }; } catch (error) { logger.warning(`Skipping creating a tracing span due to an error: ${error.message}`); - return undefined; + return { context: undefined, span: undefined }; } } - private tryProcessError(span: Span, err: any): void { + private tryProcessError(span: TracingSpan, err: any): void { try { span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, + status: "error", + error: err, }); if (err.statusCode) { @@ -156,7 +154,7 @@ export class TracingPolicy extends BaseRequestPolicy { } } - private tryProcessResponse(span: Span, response: HttpOperationResponse): void { + private tryProcessResponse(span: TracingSpan, response: HttpOperationResponse): void { try { span.setAttribute("http.status_code", response.status); const serviceRequestId = response.headers.get("x-ms-request-id"); @@ -164,7 +162,7 @@ export class TracingPolicy extends BaseRequestPolicy { span.setAttribute("serviceRequestId", serviceRequestId); } span.setStatus({ - code: SpanStatusCode.OK, + status: "success", }); span.end(); } catch (error) { diff --git a/sdk/core/core-http/src/webResource.ts b/sdk/core/core-http/src/webResource.ts index cd1378a2ff32..a1816d9a0cd2 100644 --- a/sdk/core/core-http/src/webResource.ts +++ b/sdk/core/core-http/src/webResource.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Context, SpanOptions } from "@azure/core-tracing"; +import { TracingContext } from "@azure/core-tracing"; import { HttpHeaders, HttpHeadersLike, isHttpHeadersLike } from "./httpHeaders"; import { Mapper, Serializer } from "./serializer"; import { AbortSignalLike } from "@azure/abort-controller"; @@ -142,7 +142,7 @@ export interface WebResourceLike { /** * Tracing: Context used when creating spans. */ - tracingContext?: Context; + tracingContext?: TracingContext; /** * Validates that the required properties such as method, url, headers["Content-Type"], @@ -284,15 +284,10 @@ export class WebResource implements WebResourceLike { */ onDownloadProgress?: (progress: TransferProgressEvent) => void; - /** - * Tracing: Options used to create a span when tracing is enabled. - */ - spanOptions?: SpanOptions; - /** * Tracing: Context used when creating Spans. */ - tracingContext?: Context; + tracingContext?: TracingContext; constructor( url?: string, @@ -550,10 +545,6 @@ export class WebResource implements WebResourceLike { } } - if (options.spanOptions) { - this.spanOptions = options.spanOptions; - } - if (options.tracingContext) { this.tracingContext = options.tracingContext; } @@ -711,14 +702,10 @@ export interface RequestPrepareOptions { * Allows keeping track of the progress of downloading the incoming response. */ onDownloadProgress?: (progress: TransferProgressEvent) => void; - /** - * Tracing: Options used to create a span when tracing is enabled. - */ - spanOptions?: SpanOptions; /** * Tracing: Context used when creating spans. */ - tracingContext?: Context; + tracingContext?: TracingContext; } /** @@ -778,7 +765,7 @@ export interface RequestOptionsBase { /** * Tracing: Context used when creating spans. */ - tracingContext?: Context; + tracingContext?: TracingContext; /** * May contain other properties. diff --git a/sdk/core/core-http/test/policies/tracingPolicyTests.ts b/sdk/core/core-http/test/policies/tracingPolicyTests.ts index 75a52d512ab2..0417654f7ea8 100644 --- a/sdk/core/core-http/test/policies/tracingPolicyTests.ts +++ b/sdk/core/core-http/test/policies/tracingPolicyTests.ts @@ -1,433 +1,433 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - HttpHeaders, - HttpOperationResponse, - RequestPolicy, - RequestPolicyOptions, - WebResource, -} from "../../src/coreHttp"; -import { Span, SpanOptions, Tracer, TracerProvider, trace } from "@opentelemetry/api"; -import { - SpanAttributeValue, - SpanAttributes, - SpanContext, - SpanStatus, - SpanStatusCode, - TraceFlags, - TraceState, - context, - setSpan, -} from "@azure/core-tracing"; -import { assert } from "chai"; -import sinon from "sinon"; -import { tracingPolicy } from "../../src/policies/tracingPolicy"; - -class MockSpan implements Span { - private _endCalled = false; - private _status: SpanStatus = { - code: SpanStatusCode.UNSET, - }; - private _attributes: SpanAttributes = {}; - - constructor( - private traceId: string, - private spanId: string, - private flags: TraceFlags, - private state: string, - options?: SpanOptions - ) { - this._attributes = options?.attributes || {}; - } - - addEvent(): this { - throw new Error("Method not implemented."); - } - - isRecording(): boolean { - return true; - } - - recordException(): void { - throw new Error("Method not implemented."); - } - - updateName(): this { - throw new Error("Method not implemented."); - } - - didEnd(): boolean { - return this._endCalled; - } - - end(): void { - this._endCalled = true; - } - - getStatus() { - return this._status; - } - - setStatus(status: SpanStatus) { - this._status = status; - return this; - } - - setAttributes(attributes: SpanAttributes): this { - for (const key in attributes) { - this.setAttribute(key, attributes[key]!); - } - return this; - } - - setAttribute(key: string, value: SpanAttributeValue) { - this._attributes[key] = value; - return this; - } - - getAttribute(key: string) { - return this._attributes[key]; - } - - spanContext(): SpanContext { - const state = this.state; - - const traceState = { - set(): TraceState { - /* empty */ - return traceState; - }, - unset(): TraceState { - /* empty */ - return traceState; - }, - get(): string | undefined { - return; - }, - serialize() { - return state; - }, - }; - - return { - traceId: this.traceId, - spanId: this.spanId, - traceFlags: this.flags, - traceState, - }; - } -} - -class MockTracer implements Tracer { - private spans: MockSpan[] = []; - private _startSpanCalled = false; - - constructor( - private traceId = "", - private spanId = "", - private flags = TraceFlags.NONE, - private state = "" - ) {} - - startActiveSpan(): never { - throw new Error("Method not implemented."); - } - - getStartedSpans(): MockSpan[] { - return this.spans; - } - - startSpanCalled(): boolean { - return this._startSpanCalled; - } - - startSpan(_name: string, options?: SpanOptions): MockSpan { - this._startSpanCalled = true; - const span = new MockSpan(this.traceId, this.spanId, this.flags, this.state, options); - this.spans.push(span); - return span; - } -} - -class MockTracerProvider implements TracerProvider { - private mockTracer: Tracer = new MockTracer(); - - setTracer(tracer: Tracer) { - this.mockTracer = tracer; - } - - getTracer(): Tracer { - return this.mockTracer; - } - - register() { - trace.setGlobalTracerProvider(this); - } - - disable() { - trace.disable(); - } -} - -const ROOT_SPAN = new MockSpan("root", "root", TraceFlags.SAMPLED, ""); - -describe("tracingPolicy", function () { - const TRACE_VERSION = "00"; - const mockTracerProvider = new MockTracerProvider(); - - const mockPolicy: RequestPolicy = { - sendRequest(request: WebResource): Promise { - return Promise.resolve({ - request: request, - status: 200, - headers: new HttpHeaders(), - }); - }, - }; - - beforeEach(() => { - mockTracerProvider.register(); - }); - - afterEach(() => { - mockTracerProvider.disable(); - }); - - it("will not create a span if tracingContext is missing", async () => { - const mockTracer = new MockTracer(); - const request = new WebResource(); - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isFalse(mockTracer.startSpanCalled()); - }); - - it("will create a span and correctly set trace headers if tracingContext is available", async () => { - const mockTraceId = "11111111111111111111111111111111"; - const mockSpanId = "2222222222222222"; - const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED); - mockTracerProvider.setTracer(mockTracer); - - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.isTrue(span.didEnd()); - - const expectedFlag = "01"; - - assert.equal( - request.headers.get("traceparent"), - `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` - ); - assert.notExists(request.headers.get("tracestate")); - }); - - it("will create a span and correctly set trace headers if tracingContext is available (no TraceOptions)", async () => { - const mockTraceId = "11111111111111111111111111111111"; - const mockSpanId = "2222222222222222"; - // leave out the TraceOptions - const mockTracer = new MockTracer(mockTraceId, mockSpanId); - mockTracerProvider.setTracer(mockTracer); - - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.isTrue(span.didEnd()); - assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); - assert.equal(span.getAttribute("http.status_code"), 200); - - const expectedFlag = "00"; - - assert.equal( - request.headers.get("traceparent"), - `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` - ); - assert.notExists(request.headers.get("tracestate")); - }); - - it("will create a span and correctly set trace headers if tracingContext is available (TraceState)", async () => { - const mockTraceId = "11111111111111111111111111111111"; - const mockSpanId = "2222222222222222"; - const mockTraceState = "foo=bar"; - const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); - mockTracerProvider.setTracer(mockTracer); - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.isTrue(span.didEnd()); - assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); - assert.equal(span.getAttribute("http.status_code"), 200); - - const expectedFlag = "01"; - - assert.equal( - request.headers.get("traceparent"), - `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` - ); - assert.equal(request.headers.get("tracestate"), mockTraceState); - }); - - it("will close a span if an error is encountered", async () => { - const mockTraceId = "11111111111111111111111111111111"; - const mockSpanId = "2222222222222222"; - const mockTraceState = "foo=bar"; - const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); - mockTracerProvider.setTracer(mockTracer); - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create( - { - sendRequest(requestParam: WebResource): Promise { - return Promise.reject({ - request: requestParam, - statusCode: 400, - headers: new HttpHeaders(), - message: "Bad Request.", - }); - }, - }, - new RequestPolicyOptions() - ); - try { - await policy.sendRequest(request); - throw new Error("Test Failure"); - } catch (err) { - assert.notEqual(err.message, "Test Failure"); - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.isTrue(span.didEnd()); - assert.deepEqual(span.getStatus(), { - code: SpanStatusCode.ERROR, - message: "Bad Request.", - }); - assert.equal(span.getAttribute("http.status_code"), 400); - - const expectedFlag = "01"; - - assert.equal( - request.headers.get("traceparent"), - `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` - ); - assert.equal(request.headers.get("tracestate"), mockTraceState); - } - }); - - it("will not set headers if span is a NoOpSpan", async () => { - mockTracerProvider.disable(); - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.notExists(request.headers.get("traceparent")); - assert.notExists(request.headers.get("tracestate")); - }); - - it("will not set headers if context is invalid", async () => { - // This will create a tracer that produces invalid trace-id and span-id - const mockTracer = new MockTracer("invalid", "00", TraceFlags.SAMPLED, "foo=bar"); - mockTracerProvider.setTracer(mockTracer); - - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.notExists(request.headers.get("traceparent")); - assert.notExists(request.headers.get("tracestate")); - }); - - it("will not fail the request if span setup fails", async () => { - const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); - sinon.stub(errorTracer, "startSpan").throws(new Error("Test Error")); - mockTracerProvider.setTracer(errorTracer); - - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - - const response = await policy.sendRequest(request); - assert.equal(response.status, 200); - }); - - it("will not fail the request if response processing fails", async () => { - const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); - mockTracerProvider.setTracer(errorTracer); - const errorSpan = new MockSpan("", "", TraceFlags.SAMPLED, ""); - sinon.stub(errorSpan, "end").throws(new Error("Test Error")); - sinon.stub(errorTracer, "startSpan").returns(errorSpan); - - const request = new WebResource(); - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - - const response = await policy.sendRequest(request); - assert.equal(response.status, 200); - }); - - it("will give priority to context's az.namespace over spanOptions", async () => { - const mockTracer = new MockTracer(); - mockTracerProvider.setTracer(mockTracer); - - const request = new WebResource(); - request.spanOptions = { - attributes: { "az.namespace": "value_from_span_options" }, - }; - request.tracingContext = setSpan(context.active(), ROOT_SPAN).setValue( - Symbol.for("az.namespace"), - "value_from_context" - ); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.equal(span.getAttribute("az.namespace"), "value_from_context"); - }); - - it("will use spanOptions if context does not have az.namespace", async () => { - const mockTracer = new MockTracer(); - mockTracerProvider.setTracer(mockTracer); - - const request = new WebResource(); - request.spanOptions = { - attributes: { "az.namespace": "value_from_span_options" }, - }; - request.tracingContext = setSpan(context.active(), ROOT_SPAN); - - const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - await policy.sendRequest(request); - - assert.isTrue(mockTracer.startSpanCalled()); - assert.lengthOf(mockTracer.getStartedSpans(), 1); - const span = mockTracer.getStartedSpans()[0]; - assert.equal(span.getAttribute("az.namespace"), "value_from_span_options"); - }); -}); +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT license. + +// import { +// HttpHeaders, +// HttpOperationResponse, +// RequestPolicy, +// RequestPolicyOptions, +// WebResource, +// } from "../../src/coreHttp"; +// import { Span, SpanOptions, Tracer, TracerProvider, trace } from "@opentelemetry/api"; +// import { +// SpanAttributeValue, +// SpanAttributes, +// SpanContext, +// SpanStatus, +// SpanStatusCode, +// TraceFlags, +// TraceState, +// context, +// setSpan, +// } from "@azure/core-tracing"; +// import { assert } from "chai"; +// import sinon from "sinon"; +// import { tracingPolicy } from "../../src/policies/tracingPolicy"; + +// class MockSpan implements Span { +// private _endCalled = false; +// private _status: SpanStatus = { +// code: SpanStatusCode.UNSET, +// }; +// private _attributes: SpanAttributes = {}; + +// constructor( +// private traceId: string, +// private spanId: string, +// private flags: TraceFlags, +// private state: string, +// options?: SpanOptions +// ) { +// this._attributes = options?.attributes || {}; +// } + +// addEvent(): this { +// throw new Error("Method not implemented."); +// } + +// isRecording(): boolean { +// return true; +// } + +// recordException(): void { +// throw new Error("Method not implemented."); +// } + +// updateName(): this { +// throw new Error("Method not implemented."); +// } + +// didEnd(): boolean { +// return this._endCalled; +// } + +// end(): void { +// this._endCalled = true; +// } + +// getStatus() { +// return this._status; +// } + +// setStatus(status: SpanStatus) { +// this._status = status; +// return this; +// } + +// setAttributes(attributes: SpanAttributes): this { +// for (const key in attributes) { +// this.setAttribute(key, attributes[key]!); +// } +// return this; +// } + +// setAttribute(key: string, value: SpanAttributeValue) { +// this._attributes[key] = value; +// return this; +// } + +// getAttribute(key: string) { +// return this._attributes[key]; +// } + +// spanContext(): SpanContext { +// const state = this.state; + +// const traceState = { +// set(): TraceState { +// /* empty */ +// return traceState; +// }, +// unset(): TraceState { +// /* empty */ +// return traceState; +// }, +// get(): string | undefined { +// return; +// }, +// serialize() { +// return state; +// }, +// }; + +// return { +// traceId: this.traceId, +// spanId: this.spanId, +// traceFlags: this.flags, +// traceState, +// }; +// } +// } + +// class MockTracer implements Tracer { +// private spans: MockSpan[] = []; +// private _startSpanCalled = false; + +// constructor( +// private traceId = "", +// private spanId = "", +// private flags = TraceFlags.NONE, +// private state = "" +// ) {} + +// startActiveSpan(): never { +// throw new Error("Method not implemented."); +// } + +// getStartedSpans(): MockSpan[] { +// return this.spans; +// } + +// startSpanCalled(): boolean { +// return this._startSpanCalled; +// } + +// startSpan(_name: string, options?: SpanOptions): MockSpan { +// this._startSpanCalled = true; +// const span = new MockSpan(this.traceId, this.spanId, this.flags, this.state, options); +// this.spans.push(span); +// return span; +// } +// } + +// class MockTracerProvider implements TracerProvider { +// private mockTracer: Tracer = new MockTracer(); + +// setTracer(tracer: Tracer) { +// this.mockTracer = tracer; +// } + +// getTracer(): Tracer { +// return this.mockTracer; +// } + +// register() { +// trace.setGlobalTracerProvider(this); +// } + +// disable() { +// trace.disable(); +// } +// } + +// const ROOT_SPAN = new MockSpan("root", "root", TraceFlags.SAMPLED, ""); + +// describe("tracingPolicy", function () { +// const TRACE_VERSION = "00"; +// const mockTracerProvider = new MockTracerProvider(); + +// const mockPolicy: RequestPolicy = { +// sendRequest(request: WebResource): Promise { +// return Promise.resolve({ +// request: request, +// status: 200, +// headers: new HttpHeaders(), +// }); +// }, +// }; + +// beforeEach(() => { +// mockTracerProvider.register(); +// }); + +// afterEach(() => { +// mockTracerProvider.disable(); +// }); + +// it("will not create a span if tracingContext is missing", async () => { +// const mockTracer = new MockTracer(); +// const request = new WebResource(); +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isFalse(mockTracer.startSpanCalled()); +// }); + +// it("will create a span and correctly set trace headers if tracingContext is available", async () => { +// const mockTraceId = "11111111111111111111111111111111"; +// const mockSpanId = "2222222222222222"; +// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED); +// mockTracerProvider.setTracer(mockTracer); + +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.isTrue(span.didEnd()); + +// const expectedFlag = "01"; + +// assert.equal( +// request.headers.get("traceparent"), +// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` +// ); +// assert.notExists(request.headers.get("tracestate")); +// }); + +// it("will create a span and correctly set trace headers if tracingContext is available (no TraceOptions)", async () => { +// const mockTraceId = "11111111111111111111111111111111"; +// const mockSpanId = "2222222222222222"; +// // leave out the TraceOptions +// const mockTracer = new MockTracer(mockTraceId, mockSpanId); +// mockTracerProvider.setTracer(mockTracer); + +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.isTrue(span.didEnd()); +// assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); +// assert.equal(span.getAttribute("http.status_code"), 200); + +// const expectedFlag = "00"; + +// assert.equal( +// request.headers.get("traceparent"), +// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` +// ); +// assert.notExists(request.headers.get("tracestate")); +// }); + +// it("will create a span and correctly set trace headers if tracingContext is available (TraceState)", async () => { +// const mockTraceId = "11111111111111111111111111111111"; +// const mockSpanId = "2222222222222222"; +// const mockTraceState = "foo=bar"; +// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); +// mockTracerProvider.setTracer(mockTracer); +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.isTrue(span.didEnd()); +// assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); +// assert.equal(span.getAttribute("http.status_code"), 200); + +// const expectedFlag = "01"; + +// assert.equal( +// request.headers.get("traceparent"), +// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` +// ); +// assert.equal(request.headers.get("tracestate"), mockTraceState); +// }); + +// it("will close a span if an error is encountered", async () => { +// const mockTraceId = "11111111111111111111111111111111"; +// const mockSpanId = "2222222222222222"; +// const mockTraceState = "foo=bar"; +// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); +// mockTracerProvider.setTracer(mockTracer); +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create( +// { +// sendRequest(requestParam: WebResource): Promise { +// return Promise.reject({ +// request: requestParam, +// statusCode: 400, +// headers: new HttpHeaders(), +// message: "Bad Request.", +// }); +// }, +// }, +// new RequestPolicyOptions() +// ); +// try { +// await policy.sendRequest(request); +// throw new Error("Test Failure"); +// } catch (err) { +// assert.notEqual(err.message, "Test Failure"); +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.isTrue(span.didEnd()); +// assert.deepEqual(span.getStatus(), { +// code: SpanStatusCode.ERROR, +// message: "Bad Request.", +// }); +// assert.equal(span.getAttribute("http.status_code"), 400); + +// const expectedFlag = "01"; + +// assert.equal( +// request.headers.get("traceparent"), +// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` +// ); +// assert.equal(request.headers.get("tracestate"), mockTraceState); +// } +// }); + +// it("will not set headers if span is a NoOpSpan", async () => { +// mockTracerProvider.disable(); +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.notExists(request.headers.get("traceparent")); +// assert.notExists(request.headers.get("tracestate")); +// }); + +// it("will not set headers if context is invalid", async () => { +// // This will create a tracer that produces invalid trace-id and span-id +// const mockTracer = new MockTracer("invalid", "00", TraceFlags.SAMPLED, "foo=bar"); +// mockTracerProvider.setTracer(mockTracer); + +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.notExists(request.headers.get("traceparent")); +// assert.notExists(request.headers.get("tracestate")); +// }); + +// it("will not fail the request if span setup fails", async () => { +// const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); +// sinon.stub(errorTracer, "startSpan").throws(new Error("Test Error")); +// mockTracerProvider.setTracer(errorTracer); + +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + +// const response = await policy.sendRequest(request); +// assert.equal(response.status, 200); +// }); + +// it("will not fail the request if response processing fails", async () => { +// const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); +// mockTracerProvider.setTracer(errorTracer); +// const errorSpan = new MockSpan("", "", TraceFlags.SAMPLED, ""); +// sinon.stub(errorSpan, "end").throws(new Error("Test Error")); +// sinon.stub(errorTracer, "startSpan").returns(errorSpan); + +// const request = new WebResource(); +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + +// const response = await policy.sendRequest(request); +// assert.equal(response.status, 200); +// }); + +// it("will give priority to context's az.namespace over spanOptions", async () => { +// const mockTracer = new MockTracer(); +// mockTracerProvider.setTracer(mockTracer); + +// const request = new WebResource(); +// request.spanOptions = { +// attributes: { "az.namespace": "value_from_span_options" }, +// }; +// request.tracingContext = setSpan(context.active(), ROOT_SPAN).setValue( +// Symbol.for("az.namespace"), +// "value_from_context" +// ); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.equal(span.getAttribute("az.namespace"), "value_from_context"); +// }); + +// it("will use spanOptions if context does not have az.namespace", async () => { +// const mockTracer = new MockTracer(); +// mockTracerProvider.setTracer(mockTracer); + +// const request = new WebResource(); +// request.spanOptions = { +// attributes: { "az.namespace": "value_from_span_options" }, +// }; +// request.tracingContext = setSpan(context.active(), ROOT_SPAN); + +// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); +// await policy.sendRequest(request); + +// assert.isTrue(mockTracer.startSpanCalled()); +// assert.lengthOf(mockTracer.getStartedSpans(), 1); +// const span = mockTracer.getStartedSpans()[0]; +// assert.equal(span.getAttribute("az.namespace"), "value_from_span_options"); +// }); +// }); diff --git a/sdk/eventhub/event-hubs/package.json b/sdk/eventhub/event-hubs/package.json index 273455e9cee1..a1cc7a4ea0ca 100644 --- a/sdk/eventhub/event-hubs/package.json +++ b/sdk/eventhub/event-hubs/package.json @@ -110,7 +110,7 @@ "@azure/core-amqp": "^3.0.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 6d028766d38c..ca017e974512 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -15,9 +15,9 @@ 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 { TracingSpan } from '@azure/core-tracing'; +import { TracingSpanContext } from '@azure/core-tracing'; import { WebSocketImpl } from 'rhea-promise'; import { WebSocketOptions } from '@azure/core-amqp'; @@ -86,7 +86,7 @@ export interface EventDataBatch { _generateMessage(): Buffer; readonly maxSizeInBytes: number; // @internal - readonly _messageSpanContexts: SpanContext[]; + readonly _messageSpanContexts: TracingSpanContext[]; // @internal readonly partitionId?: string; // @internal @@ -354,7 +354,7 @@ export { TokenCredential } // @public export interface TryAddOptions { // @deprecated (undocumented) - parentSpan?: Span | SpanContext; + parentSpan?: TracingSpan | TracingSpanContext; tracingOptions?: OperationTracingOptions; } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index 308685bcf518..00f5446c8c99 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -3,14 +3,14 @@ import { EventData, isAmqpAnnotatedMessage } from "../eventData"; import { - extractSpanContextFromTraceParentHeader, - getTraceParentHeader, - isSpanContextValid, + TracingSpanContext, + // extractSpanContextFromTraceParentHeader, + // getTraceParentHeader, + // isSpanContextValid, } 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 { createMessageSpan, tracingClient } from "./tracing"; /** * @internal @@ -29,7 +29,7 @@ export function instrumentEventData( options: OperationOptions, entityPath: string, host: string -): { event: EventData; spanContext: SpanContext | undefined } { +): { event: EventData; spanContext: TracingSpanContext | undefined } { const props = isAmqpAnnotatedMessage(eventData) ? eventData.applicationProperties : eventData.properties; @@ -41,7 +41,7 @@ export function instrumentEventData( return { event: eventData, spanContext: undefined }; } - const { span: messageSpan } = createMessageSpan(options, { entityPath, host }); + const { span: messageSpan, updatedOptions } = createMessageSpan(options, { entityPath, host }); try { if (!messageSpan.isRecording()) { return { @@ -50,8 +50,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 @@ -77,11 +79,13 @@ export function instrumentEventData( * @param eventData - An individual `EventData` object. * @internal */ -export function extractSpanContextFromEventData(eventData: EventData): SpanContext | undefined { +export function extractSpanContextFromEventData( + eventData: EventData +): TracingSpanContext | 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..a8fbbf55e132 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts @@ -1,23 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - Span, - SpanContext, - SpanKind, - SpanOptions, - context, - createSpanFunction, - setSpan, - setSpanContext, -} from "@azure/core-tracing"; +import { createTracingClient, TracingSpan, TracingSpanOptions } from "@azure/core-tracing"; import { EventHubConnectionConfig } from "../eventhubConnectionConfig"; import { OperationOptions } from "../util/operationOptions"; -import { TryAddOptions } from "../eventDataBatch"; -const _createSpan = createSpanFunction({ - namespace: "Microsoft.EventHub", - packagePrefix: "Azure.EventHubs", +export const tracingClient = createTracingClient({ + namespace: "Azure.EventHubs", + packageName: "@azure/event-hubs", }); /** @@ -28,9 +18,9 @@ export function createEventHubSpan( operationName: string, operationOptions: OperationOptions | undefined, connectionConfig: Pick, - additionalSpanOptions?: SpanOptions -): { span: Span; updatedOptions: OperationOptions } { - const { span, updatedOptions } = _createSpan(operationName, { + additionalSpanOptions?: TracingSpanOptions +): { span: TracingSpan; updatedOptions: OperationOptions } { + const { span, updatedOptions } = tracingClient.startSpan(operationName, { ...operationOptions, tracingOptions: { ...operationOptions?.tracingOptions, @@ -59,78 +49,6 @@ export function createMessageSpan( eventHubConfig: Pick ): ReturnType { return createEventHubSpan("message", operationOptions, eventHubConfig, { - kind: SpanKind.PRODUCER, + 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; - } - - const x = possibleSpan as Span; - return typeof x.spanContext === "function"; -} diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 37a80c9c2d83..3e64895983ad 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -3,12 +3,10 @@ import { EventData, toRheaMessage } from "./eventData"; import { MessageAnnotations, Message as RheaMessage, message } from "rhea-promise"; -import { Span, SpanContext } from "@azure/core-tracing"; import { isDefined, isObjectWithProperties } from "./util/typeGuards"; import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { ConnectionContext } from "./connectionContext"; -import { OperationTracingOptions } from "@azure/core-tracing"; -import { convertTryAddOptionsForCompatibility } from "./diagnostics/tracing"; +import { OperationTracingOptions, TracingSpan, TracingSpanContext } from "@azure/core-tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; import { throwTypeErrorIfParameterMissing } from "./util/error"; @@ -51,7 +49,7 @@ export interface TryAddOptions { /** * @deprecated Tracing options have been moved to the `tracingOptions` property. */ - parentSpan?: Span | SpanContext; + parentSpan?: TracingSpan | TracingSpanContext; } /** @@ -125,7 +123,7 @@ export interface EventDataBatch { * Used internally by the `sendBatch()` method to set up the right spans in traces if tracing is enabled. * @internal */ - readonly _messageSpanContexts: SpanContext[]; + readonly _messageSpanContexts: TracingSpanContext[]; } /** @@ -168,7 +166,7 @@ export class EventDataBatchImpl implements EventDataBatch { /** * List of 'message' span contexts. */ - private _spanContexts: SpanContext[] = []; + private _spanContexts: TracingSpanContext[] = []; /** * The message annotations to apply on the batch envelope. * This will reflect the message annotations on the first event @@ -243,7 +241,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(): TracingSpanContext[] { return this._spanContexts; } @@ -286,7 +284,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 6d2b68a00101..e70905f1795a 100644 --- a/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts @@ -12,7 +12,7 @@ import { } from "./models/public"; import { EventDataBatch, EventDataBatchImpl, isEventDataBatch } from "./eventDataBatch"; import { EventHubProperties, PartitionProperties } from "./managementClient"; -import { Link, Span, SpanContext, SpanKind, SpanStatusCode } from "@azure/core-tracing"; +import { TracingSpan, TracingSpanContext, TracingSpanLink } from "@azure/core-tracing"; import { NamedKeyCredential, SASCredential, TokenCredential } from "@azure/core-auth"; import { isCredential, isDefined } from "./util/typeGuards"; import { logErrorStackTrace, logger } from "./log"; @@ -284,7 +284,7 @@ export class EventHubProducerClient { let partitionKey: string | undefined; // link message span contexts - let spanContextsToLink: SpanContext[] = []; + let spanContextsToLink: TracingSpanContext[] = []; if (isEventDataBatch(batch)) { // For batches, partitionId and partitionKey would be set on the batch. @@ -350,12 +350,12 @@ export class EventHubProducerClient { partitionKey, retryOptions: this._clientOptions.retryOptions, }); - sendSpan.setStatus({ code: SpanStatusCode.OK }); + sendSpan.setStatus({ status: "success" }); return result; } catch (error) { sendSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, + status: "error", + error, }); throw error; } finally { @@ -431,17 +431,17 @@ export class EventHubProducerClient { private _createSendSpan( operationOptions: OperationOptions, - spanContextsToLink: SpanContext[] = [] - ): Span { - const links: Link[] = spanContextsToLink.map((context) => { + spanContextsToLink: TracingSpanContext[] = [] + ): TracingSpan { + const spanLinks: TracingSpanLink[] = spanContextsToLink.map((context) => { return { - context, + spanContext: context, }; }); const { span } = createEventHubSpan("send", operationOptions, this._context.config, { - kind: SpanKind.CLIENT, - links, + spanKind: "client", + spanLinks, }); return span; diff --git a/sdk/eventhub/event-hubs/src/managementClient.ts b/sdk/eventhub/event-hubs/src/managementClient.ts index 3712f4f534e5..995fc3c0f381 100644 --- a/sdk/eventhub/event-hubs/src/managementClient.ts +++ b/sdk/eventhub/event-hubs/src/managementClient.ts @@ -29,7 +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 {} from "@azure/core-tracing"; import { createEventHubSpan } from "./diagnostics/tracing"; import { getRetryAttemptTimeoutInMs } from "./util/retries"; import { v4 as uuid } from "uuid"; @@ -190,13 +190,10 @@ export class ManagementClient extends LinkEntity { }; logger.verbose("[%s] The hub runtime info is: %O", this._context.connectionId, runtimeInfo); - clientSpan.setStatus({ code: SpanStatusCode.OK }); + clientSpan.setStatus({ status: "success" }); return runtimeInfo; } catch (error) { - clientSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); + clientSpan.setStatus({ status: "error", error }); logger.warning( `An error occurred while getting the hub runtime information: ${error?.name}: ${error?.message}` ); @@ -261,13 +258,13 @@ export class ManagementClient extends LinkEntity { }; logger.verbose("[%s] The partition info is: %O.", this._context.connectionId, partitionInfo); - clientSpan.setStatus({ code: SpanStatusCode.OK }); + clientSpan.setStatus({ status: "success" }); return partitionInfo; } catch (error) { clientSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, + status: "error", + error, }); logger.warning( `An error occurred while getting the partition information: ${error?.name}: ${error?.message}` diff --git a/sdk/eventhub/event-hubs/src/partitionProcessor.ts b/sdk/eventhub/event-hubs/src/partitionProcessor.ts index c074a3fc7c4f..6468e7a368e3 100644 --- a/sdk/eventhub/event-hubs/src/partitionProcessor.ts +++ b/sdk/eventhub/event-hubs/src/partitionProcessor.ts @@ -11,6 +11,8 @@ import { CloseReason } from "./models/public"; import { LastEnqueuedEventProperties } from "./eventHubReceiver"; import { ReceivedEventData } from "./eventData"; import { logger } from "./log"; +import { tracingClient } from "./diagnostics/tracing"; +import { OperationOptions } from "./util/operationOptions"; /** * A checkpoint is meant to represent the last successfully processed event by the user from a particular @@ -158,8 +160,13 @@ export class PartitionProcessor implements PartitionContext { * * @param event - The received events to be processed. */ - async processEvents(events: ReceivedEventData[]): Promise { - await this._eventHandlers.processEvents(events, this); + async processEvents( + events: ReceivedEventData[], + updatedOptions: OperationOptions + ): Promise { + await tracingClient.withContext(updatedOptions.tracingOptions!.tracingContext!, async () => { + await this._eventHandlers.processEvents(events, this); + }); } /** diff --git a/sdk/eventhub/event-hubs/src/partitionPump.ts b/sdk/eventhub/event-hubs/src/partitionPump.ts index fe44c0992280..594bbc238b50 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 { TracingSpan, TracingSpanLink } from "@azure/core-tracing"; import { logErrorStackTrace, logger } from "./log"; import { AbortController } from "@azure/abort-controller"; import { CloseReason } from "./models/public"; @@ -131,13 +131,16 @@ export class PartitionPump { lastSeenSequenceNumber = receivedEvents[receivedEvents.length - 1].sequenceNumber; } - const span = createProcessingSpan( + const { span, updatedOptions } = createProcessingSpan( receivedEvents, this._context.config, this._processorOptions ); - await trace(() => this._partitionProcessor.processEvents(receivedEvents), span); + await trace( + () => this._partitionProcessor.processEvents(receivedEvents, updatedOptions), + span + ); } catch (err) { // check if this pump is still receiving // it may not be if the EventProcessor was stopped during processEvents @@ -212,8 +215,8 @@ export function createProcessingSpan( receivedEvents: ReceivedEventData[], eventHubProperties: Pick, options?: OperationOptions -): Span { - const links: Link[] = []; +): { span: TracingSpan; updatedOptions: OperationOptions } { + const links: TracingSpanLink[] = []; for (const receivedEvent of receivedEvents) { const spanContext = extractSpanContextFromEventData(receivedEvent); @@ -223,32 +226,32 @@ export function createProcessingSpan( } links.push({ - context: spanContext, + spanContext, attributes: { enqueuedTime: receivedEvent.enqueuedTimeUtc.getTime(), }, }); } - const { span } = createEventHubSpan("process", options, eventHubProperties, { - kind: SpanKind.CONSUMER, - links, + const { span, updatedOptions } = createEventHubSpan("process", options, eventHubProperties, { + spanKind: "consumer", + spanLinks: links, }); - return span; + return { span, updatedOptions }; } /** * @internal */ -export async function trace(fn: () => Promise, span: Span): Promise { +export async function trace(fn: () => Promise, span: TracingSpan): Promise { try { await fn(); - span.setStatus({ code: SpanStatusCode.OK }); + span.setStatus({ status: "success" }); } catch (err) { span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, + status: "error", + error: err, }); throw err; } finally { diff --git a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts index 8fd955161266..5081ee812a83 100644 --- a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts @@ -1,478 +1,477 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils"; -import { - EventData, - EventHubConsumerClient, - EventHubProducerClient, - EventHubProperties, - ReceivedEventData, - Subscription, -} from "../../src"; -import { - TRACEPARENT_PROPERTY, - 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"; - -const should = chai.should(); -chai.use(chaiAsPromised); -const debug = debugModule("azure:event-hubs:misc-spec"); - -testWithServiceTypes((serviceVersion) => { - const env = getEnvVars(); - if (serviceVersion === "mock") { - let service: ReturnType; - before("Starting mock service", () => { - service = createMockServer(); - return service.start(); - }); - - after("Stopping mock service", () => { - return service?.stop(); - }); - } - - describe("Misc tests", function (): void { - const service = { - connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - path: env[EnvVarKeys.EVENTHUB_NAME], - }; - let consumerClient: EventHubConsumerClient; - let producerClient: EventHubProducerClient; - let hubInfo: EventHubProperties; - let partitionId: string; - let lastEnqueuedOffset: number; - - before("validate environment", async function (): Promise { - should.exist( - env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." - ); - should.exist( - env[EnvVarKeys.EVENTHUB_NAME], - "define EVENTHUB_NAME in your environment before running integration tests." - ); - }); - - beforeEach(async () => { - debug("Creating the clients.."); - producerClient = new EventHubProducerClient(service.connectionString, service.path); - consumerClient = new EventHubConsumerClient( - EventHubConsumerClient.defaultConsumerGroupName, - service.connectionString, - service.path - ); - hubInfo = await consumerClient.getEventHubProperties(); - partitionId = hubInfo.partitionIds[0]; - lastEnqueuedOffset = (await consumerClient.getPartitionProperties(partitionId)) - .lastEnqueuedOffset; - }); - - afterEach(async () => { - debug("Closing the clients.."); - await producerClient.close(); - await consumerClient.close(); - }); - - it("should be able to send and receive a large message correctly", async function (): Promise { - const bodysize = 220 * 1024; - const msgString = "A".repeat(220 * 1024); - const msgBody = Buffer.from(msgString); - const obj: EventData = { body: msgBody }; - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - debug("Sending one message with %d bytes.", bodysize); - await producerClient.sendBatch([obj], { partitionId }); - debug("Successfully sent the large message."); - - let subscription: Subscription | undefined; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - should.exist(data); - should.equal(data.length, 1); - should.equal(data[0].body.toString(), msgString); - should.not.exist((data[0].properties || {}).message_id); - resolve(); - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - }); - - it("should be able to send and receive a JSON object as a message correctly", async function (): Promise { - const msgBody = { - id: "123-456-789", - weight: 10, - isBlue: true, - siblings: [ - { - id: "098-789-564", - weight: 20, - isBlue: false, - }, - ], - }; - const obj: EventData = { body: msgBody }; - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - debug("Sending one message %O", obj); - await producerClient.sendBatch([obj], { partitionId }); - debug("Successfully sent the large message."); - - let subscription: Subscription | undefined; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - should.exist(data); - should.equal(data.length, 1); - assert.deepEqual(data[0].body, msgBody); - should.not.exist((data[0].properties || {}).message_id); - resolve(); - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - }); - - it("should be able to send and receive an array as a message correctly", async function (): Promise { - const msgBody = [ - { - id: "098-789-564", - weight: 20, - isBlue: false, - }, - 10, - 20, - "some string", - ]; - const obj: EventData = { body: msgBody, properties: { message_id: uuid() } }; - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - debug("Sending one message %O", obj); - await producerClient.sendBatch([obj], { partitionId }); - debug("Successfully sent the large message."); - - let subscription: Subscription | undefined; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - should.exist(data); - should.equal(data.length, 1); - assert.deepEqual(data[0].body, msgBody); - assert.strictEqual(data[0].properties!.message_id, obj.properties!.message_id); - resolve(); - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - }); - - it("should be able to send a boolean as a message correctly", async function (): Promise { - const msgBody = true; - const obj: EventData = { body: msgBody }; - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - debug("Sending one message %O", obj); - await producerClient.sendBatch([obj], { partitionId }); - debug("Successfully sent the large message."); - - let subscription: Subscription | undefined; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - should.exist(data); - should.equal(data.length, 1); - assert.deepEqual(data[0].body, msgBody); - should.not.exist((data[0].properties || {}).message_id); - resolve(); - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - }); - - it("should be able to send and receive batched messages correctly ", async function (): Promise { - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - const messageCount = 5; - const d: EventData[] = []; - for (let i = 0; i < messageCount; i++) { - const obj: EventData = { body: `Hello EH ${i}` }; - d.push(obj); - } - - await producerClient.sendBatch(d, { partitionId }); - debug("Successfully sent 5 messages batched together."); - - let subscription: Subscription | undefined; - const receivedMsgs: ReceivedEventData[] = []; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - receivedMsgs.push(...data); - if (receivedMsgs.length === 5) { - resolve(); - } - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - receivedMsgs.length.should.equal(5); - for (const message of receivedMsgs) { - should.not.exist((message.properties || {}).message_id); - } - }); - - it("should be able to send and receive batched messages as JSON objects correctly ", async function (): Promise { - debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); - const messageCount = 5; - const d: EventData[] = []; - for (let i = 0; i < messageCount; i++) { - const obj: EventData = { - body: { - id: "123-456-789", - count: i, - weight: 10, - isBlue: true, - siblings: [ - { - id: "098-789-564", - weight: 20, - isBlue: false, - }, - ], - }, - properties: { - message_id: uuid(), - }, - }; - d.push(obj); - } - - await producerClient.sendBatch(d, { partitionId }); - debug("Successfully sent 5 messages batched together."); - - let subscription: Subscription | undefined; - const receivedMsgs: ReceivedEventData[] = []; - await new Promise((resolve, reject) => { - subscription = consumerClient.subscribe( - partitionId, - { - processEvents: async (data) => { - debug("received message: ", data.length); - receivedMsgs.push(...data); - if (receivedMsgs.length === 5) { - resolve(); - } - }, - processError: async (err) => { - reject(err); - }, - }, - { - startPosition: { offset: lastEnqueuedOffset }, - } - ); - }); - await subscription!.close(); - should.equal(receivedMsgs[0].body.count, 0); - should.equal(receivedMsgs.length, 5); - for (const [index, message] of receivedMsgs.entries()) { - assert.strictEqual(message.properties!.message_id, d[index].properties!.message_id); - } - }); - - it("should consistently send messages with partitionkey to a partitionId", async function (): Promise { - const { subscriptionEventHandler, startPosition } = - await SubscriptionHandlerForTests.startingFromHere(consumerClient); - - const msgToSendCount = 50; - debug("Sending %d messages.", msgToSendCount); - - function getRandomInt(max: number): number { - return Math.floor(Math.random() * Math.floor(max)); - } - - const senderPromises = []; - - for (let i = 0; i < msgToSendCount; i++) { - const partitionKey = getRandomInt(10); - senderPromises.push( - producerClient.sendBatch([{ body: "Hello EventHub " + i }], { - partitionKey: partitionKey.toString(), - }) - ); - } - - await Promise.all(senderPromises); - - debug("Starting to receive all messages from each partition."); - const partitionMap: any = {}; - - let subscription: Subscription | undefined = undefined; - - try { - subscription = consumerClient.subscribe(subscriptionEventHandler, { - startPosition, - }); - const receivedEvents = await subscriptionEventHandler.waitForFullEvents( - hubInfo.partitionIds, - msgToSendCount - ); - - for (const d of receivedEvents) { - debug(">>>> _raw_amqp_mesage: ", (d as any)._raw_amqp_mesage); - const pk = d.event.partitionKey as string; - debug("pk: ", pk); - - if (partitionMap[pk] && partitionMap[pk] !== d.partitionId) { - debug( - `#### Error: Received a message from partition ${d.partitionId} with partition key ${pk}, whereas the same key was observed on partition ${partitionMap[pk]} before.` - ); - assert(partitionMap[pk] === d.partitionId); - } - partitionMap[pk] = d.partitionId; - debug("partitionMap ", partitionMap); - } - } finally { - if (subscription) { - await subscription.close(); - } - await consumerClient.close(); - } - }); - }).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"; - const eventData: ReceivedEventData = { - body: "This is a test.", - enqueuedTimeUtc: new Date(), - offset: 0, - sequenceNumber: 0, - partitionKey: null, - properties: { - [TRACEPARENT_PROPERTY]: `00-${traceId}-${spanId}-${flags}`, - }, - 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." - ); - }); - - it("should return undefined when EventData is not instrumented", function () { - const eventData: ReceivedEventData = { - body: "This is a test.", - enqueuedTimeUtc: new Date(), - offset: 0, - sequenceNumber: 0, - partitionKey: null, - getRawAmqpMessage() { - return {} as any; - }, - }; - - const spanContext = extractSpanContextFromEventData(eventData); - - should.not.exist( - spanContext, - `Missing property "${TRACEPARENT_PROPERTY}" should return undefined spanContext.` - ); - }); - }); -}); +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT license. + +// import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils"; +// import { +// EventData, +// EventHubConsumerClient, +// EventHubProducerClient, +// EventHubProperties, +// ReceivedEventData, +// Subscription, +// } from "../../src"; +// import { +// TRACEPARENT_PROPERTY, +// extractSpanContextFromEventData, +// } from "../../src/diagnostics/instrumentEventData"; +// import { SubscriptionHandlerForTests } from "../public/utils/subscriptionHandlerForTests"; +// 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"; + +// const should = chai.should(); +// chai.use(chaiAsPromised); +// const debug = debugModule("azure:event-hubs:misc-spec"); + +// testWithServiceTypes((serviceVersion) => { +// const env = getEnvVars(); +// if (serviceVersion === "mock") { +// let service: ReturnType; +// before("Starting mock service", () => { +// service = createMockServer(); +// return service.start(); +// }); + +// after("Stopping mock service", () => { +// return service?.stop(); +// }); +// } + +// describe("Misc tests", function (): void { +// const service = { +// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// path: env[EnvVarKeys.EVENTHUB_NAME], +// }; +// let consumerClient: EventHubConsumerClient; +// let producerClient: EventHubProducerClient; +// let hubInfo: EventHubProperties; +// let partitionId: string; +// let lastEnqueuedOffset: number; + +// before("validate environment", async function (): Promise { +// should.exist( +// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." +// ); +// should.exist( +// env[EnvVarKeys.EVENTHUB_NAME], +// "define EVENTHUB_NAME in your environment before running integration tests." +// ); +// }); + +// beforeEach(async () => { +// debug("Creating the clients.."); +// producerClient = new EventHubProducerClient(service.connectionString, service.path); +// consumerClient = new EventHubConsumerClient( +// EventHubConsumerClient.defaultConsumerGroupName, +// service.connectionString, +// service.path +// ); +// hubInfo = await consumerClient.getEventHubProperties(); +// partitionId = hubInfo.partitionIds[0]; +// lastEnqueuedOffset = (await consumerClient.getPartitionProperties(partitionId)) +// .lastEnqueuedOffset; +// }); + +// afterEach(async () => { +// debug("Closing the clients.."); +// await producerClient.close(); +// await consumerClient.close(); +// }); + +// it("should be able to send and receive a large message correctly", async function (): Promise { +// const bodysize = 220 * 1024; +// const msgString = "A".repeat(220 * 1024); +// const msgBody = Buffer.from(msgString); +// const obj: EventData = { body: msgBody }; +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// debug("Sending one message with %d bytes.", bodysize); +// await producerClient.sendBatch([obj], { partitionId }); +// debug("Successfully sent the large message."); + +// let subscription: Subscription | undefined; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// should.exist(data); +// should.equal(data.length, 1); +// should.equal(data[0].body.toString(), msgString); +// should.not.exist((data[0].properties || {}).message_id); +// resolve(); +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// }); + +// it("should be able to send and receive a JSON object as a message correctly", async function (): Promise { +// const msgBody = { +// id: "123-456-789", +// weight: 10, +// isBlue: true, +// siblings: [ +// { +// id: "098-789-564", +// weight: 20, +// isBlue: false, +// }, +// ], +// }; +// const obj: EventData = { body: msgBody }; +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// debug("Sending one message %O", obj); +// await producerClient.sendBatch([obj], { partitionId }); +// debug("Successfully sent the large message."); + +// let subscription: Subscription | undefined; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// should.exist(data); +// should.equal(data.length, 1); +// assert.deepEqual(data[0].body, msgBody); +// should.not.exist((data[0].properties || {}).message_id); +// resolve(); +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// }); + +// it("should be able to send and receive an array as a message correctly", async function (): Promise { +// const msgBody = [ +// { +// id: "098-789-564", +// weight: 20, +// isBlue: false, +// }, +// 10, +// 20, +// "some string", +// ]; +// const obj: EventData = { body: msgBody, properties: { message_id: uuid() } }; +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// debug("Sending one message %O", obj); +// await producerClient.sendBatch([obj], { partitionId }); +// debug("Successfully sent the large message."); + +// let subscription: Subscription | undefined; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// should.exist(data); +// should.equal(data.length, 1); +// assert.deepEqual(data[0].body, msgBody); +// assert.strictEqual(data[0].properties!.message_id, obj.properties!.message_id); +// resolve(); +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// }); + +// it("should be able to send a boolean as a message correctly", async function (): Promise { +// const msgBody = true; +// const obj: EventData = { body: msgBody }; +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// debug("Sending one message %O", obj); +// await producerClient.sendBatch([obj], { partitionId }); +// debug("Successfully sent the large message."); + +// let subscription: Subscription | undefined; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// should.exist(data); +// should.equal(data.length, 1); +// assert.deepEqual(data[0].body, msgBody); +// should.not.exist((data[0].properties || {}).message_id); +// resolve(); +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// }); + +// it("should be able to send and receive batched messages correctly ", async function (): Promise { +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// const messageCount = 5; +// const d: EventData[] = []; +// for (let i = 0; i < messageCount; i++) { +// const obj: EventData = { body: `Hello EH ${i}` }; +// d.push(obj); +// } + +// await producerClient.sendBatch(d, { partitionId }); +// debug("Successfully sent 5 messages batched together."); + +// let subscription: Subscription | undefined; +// const receivedMsgs: ReceivedEventData[] = []; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// receivedMsgs.push(...data); +// if (receivedMsgs.length === 5) { +// resolve(); +// } +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// receivedMsgs.length.should.equal(5); +// for (const message of receivedMsgs) { +// should.not.exist((message.properties || {}).message_id); +// } +// }); + +// it("should be able to send and receive batched messages as JSON objects correctly ", async function (): Promise { +// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); +// const messageCount = 5; +// const d: EventData[] = []; +// for (let i = 0; i < messageCount; i++) { +// const obj: EventData = { +// body: { +// id: "123-456-789", +// count: i, +// weight: 10, +// isBlue: true, +// siblings: [ +// { +// id: "098-789-564", +// weight: 20, +// isBlue: false, +// }, +// ], +// }, +// properties: { +// message_id: uuid(), +// }, +// }; +// d.push(obj); +// } + +// await producerClient.sendBatch(d, { partitionId }); +// debug("Successfully sent 5 messages batched together."); + +// let subscription: Subscription | undefined; +// const receivedMsgs: ReceivedEventData[] = []; +// await new Promise((resolve, reject) => { +// subscription = consumerClient.subscribe( +// partitionId, +// { +// processEvents: async (data) => { +// debug("received message: ", data.length); +// receivedMsgs.push(...data); +// if (receivedMsgs.length === 5) { +// resolve(); +// } +// }, +// processError: async (err) => { +// reject(err); +// }, +// }, +// { +// startPosition: { offset: lastEnqueuedOffset }, +// } +// ); +// }); +// await subscription!.close(); +// should.equal(receivedMsgs[0].body.count, 0); +// should.equal(receivedMsgs.length, 5); +// for (const [index, message] of receivedMsgs.entries()) { +// assert.strictEqual(message.properties!.message_id, d[index].properties!.message_id); +// } +// }); + +// it("should consistently send messages with partitionkey to a partitionId", async function (): Promise { +// const { subscriptionEventHandler, startPosition } = +// await SubscriptionHandlerForTests.startingFromHere(consumerClient); + +// const msgToSendCount = 50; +// debug("Sending %d messages.", msgToSendCount); + +// function getRandomInt(max: number): number { +// return Math.floor(Math.random() * Math.floor(max)); +// } + +// const senderPromises = []; + +// for (let i = 0; i < msgToSendCount; i++) { +// const partitionKey = getRandomInt(10); +// senderPromises.push( +// producerClient.sendBatch([{ body: "Hello EventHub " + i }], { +// partitionKey: partitionKey.toString(), +// }) +// ); +// } + +// await Promise.all(senderPromises); + +// debug("Starting to receive all messages from each partition."); +// const partitionMap: any = {}; + +// let subscription: Subscription | undefined = undefined; + +// try { +// subscription = consumerClient.subscribe(subscriptionEventHandler, { +// startPosition, +// }); +// const receivedEvents = await subscriptionEventHandler.waitForFullEvents( +// hubInfo.partitionIds, +// msgToSendCount +// ); + +// for (const d of receivedEvents) { +// debug(">>>> _raw_amqp_mesage: ", (d as any)._raw_amqp_mesage); +// const pk = d.event.partitionKey as string; +// debug("pk: ", pk); + +// if (partitionMap[pk] && partitionMap[pk] !== d.partitionId) { +// debug( +// `#### Error: Received a message from partition ${d.partitionId} with partition key ${pk}, whereas the same key was observed on partition ${partitionMap[pk]} before.` +// ); +// assert(partitionMap[pk] === d.partitionId); +// } +// partitionMap[pk] = d.partitionId; +// debug("partitionMap ", partitionMap); +// } +// } finally { +// if (subscription) { +// await subscription.close(); +// } +// await consumerClient.close(); +// } +// }); +// }).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"; +// const eventData: ReceivedEventData = { +// body: "This is a test.", +// enqueuedTimeUtc: new Date(), +// offset: 0, +// sequenceNumber: 0, +// partitionKey: null, +// properties: { +// [TRACEPARENT_PROPERTY]: `00-${traceId}-${spanId}-${flags}`, +// }, +// 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." +// ); +// }); + +// it("should return undefined when EventData is not instrumented", function () { +// const eventData: ReceivedEventData = { +// body: "This is a test.", +// enqueuedTimeUtc: new Date(), +// offset: 0, +// sequenceNumber: 0, +// partitionKey: null, +// getRawAmqpMessage() { +// return {} as any; +// }, +// }; + +// const spanContext = extractSpanContextFromEventData(eventData); + +// should.not.exist( +// spanContext, +// `Missing property "${TRACEPARENT_PROPERTY}" should return undefined spanContext.` +// ); +// }); +// }); +// }); diff --git a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts index 413d198094c5..4b5880d87792 100644 --- a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts @@ -1,165 +1,165 @@ -// 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 { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; - -const should = chai.should(); - -testWithServiceTypes(() => { - describe("PartitionPump", () => { - 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, - }); - - 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()), - }, - }, - "entityPath", - "host" - ).event as ReceivedEventData, - { properties: {}, ...requiredEventProperties }, // no diagnostic ID means it gets skipped - instrumentEventData( - { ...requiredEventProperties }, - { - tracingOptions: { - tracingContext: setSpanContext(context.active(), thirdEvent.spanContext()), - }, - }, - "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); - }); - }); - }); -}); +// // 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 { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; + +// const should = chai.should(); + +// testWithServiceTypes(() => { +// describe("PartitionPump", () => { +// 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, +// }); + +// 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()), +// }, +// }, +// "entityPath", +// "host" +// ).event as ReceivedEventData, +// { properties: {}, ...requiredEventProperties }, // no diagnostic ID means it gets skipped +// instrumentEventData( +// { ...requiredEventProperties }, +// { +// tracingOptions: { +// tracingContext: setSpanContext(context.active(), thirdEvent.spanContext()), +// }, +// }, +// "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); +// }); +// }); +// }); +// }); diff --git a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts index 2df75bcb862b..3c8f3acaf0f2 100644 --- a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts @@ -1,1269 +1,1269 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - EnvVarKeys, - getEnvVars, - getStartingPositionsForTests, - setTracerForTest, -} 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 { 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"; -import debugModule from "debug"; -import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; - -const should = chai.should(); -chai.use(chaiAsPromised); -const debug = debugModule("azure:event-hubs:sender-spec"); - -testWithServiceTypes((serviceVersion) => { - const env = getEnvVars(); - if (serviceVersion === "mock") { - let service: ReturnType; - before("Starting mock service", () => { - service = createMockServer(); - return service.start(); - }); - - after("Stopping mock service", () => { - return service?.stop(); - }); - } - - describe("EventHub Sender", function (): void { - const service = { - connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - path: env[EnvVarKeys.EVENTHUB_NAME], - }; - let producerClient: EventHubProducerClient; - let consumerClient: EventHubConsumerClient; - let startPosition: { [partitionId: string]: EventPosition }; - - before("validate environment", function (): void { - should.exist( - env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." - ); - should.exist( - env[EnvVarKeys.EVENTHUB_NAME], - "define EVENTHUB_NAME in your environment before running integration tests." - ); - }); - - beforeEach(async () => { - debug("Creating the clients.."); - producerClient = new EventHubProducerClient(service.connectionString, service.path); - consumerClient = new EventHubConsumerClient( - EventHubConsumerClient.defaultConsumerGroupName, - service.connectionString, - service.path - ); - startPosition = await getStartingPositionsForTests(consumerClient); - }); - - afterEach(async () => { - debug("Closing the clients.."); - await producerClient.close(); - await consumerClient.close(); - }); - - describe("Create batch", function (): void { - describe("tryAdd", function () { - it("doesn't grow if invalid events are added", async () => { - const batch = await producerClient.createBatch({ maxSizeInBytes: 20 }); - const event = { body: Buffer.alloc(30).toString() }; - - const numToAdd = 5; - let failures = 0; - for (let i = 0; i < numToAdd; i++) { - if (!batch.tryAdd(event)) { - failures++; - } - } - - failures.should.equal(5); - batch.sizeInBytes.should.equal(0); - }); - }); - - it("partitionId is set as expected", async () => { - const batch = await producerClient.createBatch({ - partitionId: "0", - }); - should.equal(batch.partitionId, "0"); - }); - - it("partitionId is set as expected when it is 0 i.e. falsy", async () => { - const batch = await producerClient.createBatch({ - // @ts-expect-error Testing the value 0 is not ignored. - partitionId: 0, - }); - should.equal(batch.partitionId, "0"); - }); - - it("partitionKey is set as expected", async () => { - const batch = await producerClient.createBatch({ - partitionKey: "boo", - }); - should.equal(batch.partitionKey, "boo"); - }); - - it("partitionKey is set as expected when it is 0 i.e. falsy", async () => { - const batch = await producerClient.createBatch({ - // @ts-expect-error Testing the value 0 is not ignored. - partitionKey: 0, - }); - should.equal(batch.partitionKey, "0"); - }); - - it("maxSizeInBytes is set as expected", async () => { - const batch = await producerClient.createBatch({ maxSizeInBytes: 30 }); - should.equal(batch.maxSizeInBytes, 30); - }); - - it("should be sent successfully", async function (): Promise { - const list = ["Albert", `${Buffer.from("Mike".repeat(1300000))}`, "Marie"]; - - const batch = await producerClient.createBatch({ - partitionId: "0", - }); - - batch.partitionId!.should.equal("0"); - should.not.exist(batch.partitionKey); - batch.maxSizeInBytes.should.be.gt(0); - - should.equal(batch.tryAdd({ body: list[0] }), true); - should.equal(batch.tryAdd({ body: list[1] }), false); // The Mike message will be rejected - it's over the limit. - should.equal(batch.tryAdd({ body: list[2] }), true); // Marie should get added"; - - const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( - producerClient - ); - - const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { - startPosition, - }); - await producerClient.sendBatch(batch); - - let receivedEvents; - - try { - receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); - } finally { - await subscriber.close(); - } - - // Mike didn't make it - the message was too big for the batch - // and was rejected above. - [list[0], list[2]].should.be.deep.eq( - receivedEvents.map((event) => event.body), - "Received messages should be equal to our sent messages" - ); - }); - - it("should be sent successfully when partitionId is 0 i.e. falsy", async function (): Promise { - const list = ["Albert", "Marie"]; - - const batch = await producerClient.createBatch({ - // @ts-expect-error Testing the value 0 is not ignored. - partitionId: 0, - }); - - batch.partitionId!.should.equal("0"); - should.not.exist(batch.partitionKey); - batch.maxSizeInBytes.should.be.gt(0); - - should.equal(batch.tryAdd({ body: list[0] }), true); - should.equal(batch.tryAdd({ body: list[1] }), true); - - const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( - producerClient - ); - - const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { - startPosition, - }); - await producerClient.sendBatch(batch); - - let receivedEvents; - - try { - receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); - } finally { - await subscriber.close(); - } - - list.should.be.deep.eq( - receivedEvents.map((event) => event.body), - "Received messages should be equal to our sent messages" - ); - }); - - it("should be sent successfully when partitionKey is 0 i.e. falsy", async function (): Promise { - const list = ["Albert", "Marie"]; - - const batch = await producerClient.createBatch({ - // @ts-expect-error Testing the value 0 is not ignored. - partitionKey: 0, - }); - - batch.partitionKey!.should.equal("0"); - should.not.exist(batch.partitionId); - batch.maxSizeInBytes.should.be.gt(0); - - should.equal(batch.tryAdd({ body: list[0] }), true); - should.equal(batch.tryAdd({ body: list[1] }), true); - - const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( - producerClient - ); - - const subscriber = consumerClient.subscribe(subscriptionEventHandler, { - startPosition, - }); - await producerClient.sendBatch(batch); - - let receivedEvents; - const allPartitionIds = await producerClient.getPartitionIds(); - try { - receivedEvents = await subscriptionEventHandler.waitForEvents(allPartitionIds, 2); - } finally { - await subscriber.close(); - } - - list.should.be.deep.eq( - receivedEvents.map((event) => event.body), - "Received messages should be equal to our sent messages" - ); - }); - - it("should be sent successfully with properties", async function (): Promise { - const properties = { test: "super" }; - const list = [ - { body: "Albert-With-Properties", properties }, - { body: "Mike-With-Properties", properties }, - { body: "Marie-With-Properties", properties }, - ]; - - const batch = await producerClient.createBatch({ - partitionId: "0", - }); - - batch.maxSizeInBytes.should.be.gt(0); - - should.equal(batch.tryAdd(list[0]), true); - should.equal(batch.tryAdd(list[1]), true); - should.equal(batch.tryAdd(list[2]), true); - - const receivedEvents: ReceivedEventData[] = []; - let waitUntilEventsReceivedResolver: (value?: any) => void; - const waitUntilEventsReceived = new Promise( - (resolve) => (waitUntilEventsReceivedResolver = resolve) - ); - - const sequenceNumber = (await consumerClient.getPartitionProperties("0")) - .lastEnqueuedSequenceNumber; - - const subscriber = consumerClient.subscribe( - "0", - { - async processError() { - /* no-op */ - }, - async processEvents(events) { - receivedEvents.push(...events); - if (receivedEvents.length >= 3) { - waitUntilEventsReceivedResolver(); - } - }, - }, - { - startPosition: { - sequenceNumber, - }, - maxBatchSize: 3, - } - ); - - await producerClient.sendBatch(batch); - await waitUntilEventsReceived; - await subscriber.close(); - - sequenceNumber.should.be.lessThan(receivedEvents[0].sequenceNumber); - sequenceNumber.should.be.lessThan(receivedEvents[1].sequenceNumber); - sequenceNumber.should.be.lessThan(receivedEvents[2].sequenceNumber); - - [list[0], list[1], list[2]].should.be.deep.eq( - receivedEvents.map((event) => { - return { - body: event.body, - properties: event.properties, - }; - }), - "Received messages should be equal to our sent messages" - ); - }); - - 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", - }); - - for (let i = 0; i < 2; i++) { - eventDataBatch.tryAdd( - { body: `${list[i].name}` }, - { - tracingOptions: { - tracingContext: setSpan(context.active(), 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: [], - }, - { - 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("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) { - eventDataBatch.tryAdd(event); - } - - should.equal(eventDataBatch.count, 2, "Unexpected number of events in batch."); - should.equal( - eventDataBatch["_messageSpanContexts"].length, - 0, - "Unexpected number of span contexts in batch." - ); - }); - - function legacyOptionsUsingSpanContext( - rootSpan: TestSpan - ): Pick { - return { - parentSpan: rootSpan.spanContext(), - }; - } - - function legacyOptionsUsingSpan(rootSpan: TestSpan): Pick { - return { - parentSpan: rootSpan, - }; - } - - function modernOptions(rootSpan: TestSpan): OperationOptions { - return { - tracingOptions: { - tracingContext: setSpan(context.active(), rootSpan), - }, - }; - } - - [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" }]; - - 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("with partition key should be sent successfully.", async function (): Promise { - const eventDataBatch = await producerClient.createBatch({ partitionKey: "1" }); - for (let i = 0; i < 5; i++) { - eventDataBatch.tryAdd({ body: `Hello World ${i}` }); - } - await producerClient.sendBatch(eventDataBatch); - }); - - it("with max message size should be sent successfully.", async function (): Promise { - const eventDataBatch = await producerClient.createBatch({ - maxSizeInBytes: 5000, - partitionId: "0", - }); - const message = { body: `${Buffer.from("Z".repeat(4096))}` }; - for (let i = 1; i <= 3; i++) { - const isAdded = eventDataBatch.tryAdd(message); - if (!isAdded) { - debug(`Unable to add ${i} event to the batch`); - break; - } - } - await producerClient.sendBatch(eventDataBatch); - eventDataBatch.count.should.equal(1); - }); - }); - - describe("Multiple sendBatch calls", function (): void { - it("should be sent successfully in parallel", async function (): Promise { - const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( - consumerClient - ); - - const promises = []; - for (let i = 0; i < 5; i++) { - promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); - } - await Promise.all(promises); - - const subscription = await consumerClient.subscribe(subscriptionEventHandler, { - startPosition, - }); - - try { - const events = await subscriptionEventHandler.waitForEvents( - await consumerClient.getPartitionIds({}), - 5 - ); - - // we've allowed the server to choose which partition the messages are distributed to - // so our expectation here is just that all the bodies have arrived - const bodiesOnly = events.map((evt) => evt.body); - bodiesOnly.sort(); - - bodiesOnly.should.deep.equal([ - "Hello World 0", - "Hello World 1", - "Hello World 2", - "Hello World 3", - "Hello World 4", - ]); - } finally { - subscription.close(); - } - }); - - it("should be sent successfully in parallel, even when exceeding max event listener count of 1000", async function (): Promise { - const senderCount = 1200; - try { - const promises = []; - for (let i = 0; i < senderCount; i++) { - promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); - } - await Promise.all(promises); - } catch (err) { - debug("An error occurred while running the test: ", err); - throw err; - } - }); - - it("should be sent successfully in parallel by multiple clients", async function (): Promise { - const senderCount = 3; - try { - const promises = []; - for (let i = 0; i < senderCount; i++) { - if (i === 0) { - debug(">>>>> Sending a message to partition %d", i); - promises.push( - await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "0" }) - ); - } else if (i === 1) { - debug(">>>>> Sending a message to partition %d", i); - promises.push( - await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "1" }) - ); - } else { - debug(">>>>> Sending a message to the hub when i == %d", i); - promises.push(await producerClient.sendBatch([{ body: `Hello World ${i}` }])); - } - } - await Promise.all(promises); - } catch (err) { - debug("An error occurred while running the test: ", err); - throw err; - } - }); - - it("should fail when a message greater than 1 MB is sent and succeed when a normal message is sent after that on the same link.", async function (): Promise { - const data: EventData = { - body: Buffer.from("Z".repeat(1300000)), - }; - try { - debug("Sending a message of 300KB..."); - await producerClient.sendBatch([data], { partitionId: "0" }); - 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 - ); - } - await producerClient.sendBatch([{ body: "Hello World EventHub!!" }], { partitionId: "0" }); - 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"); - - 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: [ - { - 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: [], - }, - ], - }, - ], - }; - - 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); - } - }); - - it("can be manually traced", async function (): Promise { - const { tracer, resetTracer } = setTracerForTest(); - - const rootSpan = tracer.startSpan("root"); - - 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), - }, - }); - 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.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."); - - 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(); - }); - - 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, { - 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(); - }); - - 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." - ); - } - }); - }); - - 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( - "partitionId and partitionKey cannot both be set when creating a batch" - ); - } - }); - - 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( - "partitionId and partitionKey cannot both be set when creating a batch" - ); - } - }); - - 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( - "partitionId and partitionKey cannot both be set when creating a batch" - ); - } - }); - - 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 - ); - } - }); - }); - 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"); - } - }); - }); - - 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("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 - ); - } - }); - }); - }); - }); - }).timeout(20000); -}); +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT license. + +// import { +// EnvVarKeys, +// getEnvVars, +// getStartingPositionsForTests, +// setTracerForTest, +// } 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 { 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"; +// import debugModule from "debug"; +// import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; + +// const should = chai.should(); +// chai.use(chaiAsPromised); +// const debug = debugModule("azure:event-hubs:sender-spec"); + +// testWithServiceTypes((serviceVersion) => { +// const env = getEnvVars(); +// if (serviceVersion === "mock") { +// let service: ReturnType; +// before("Starting mock service", () => { +// service = createMockServer(); +// return service.start(); +// }); + +// after("Stopping mock service", () => { +// return service?.stop(); +// }); +// } + +// describe("EventHub Sender", function (): void { +// const service = { +// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// path: env[EnvVarKeys.EVENTHUB_NAME], +// }; +// let producerClient: EventHubProducerClient; +// let consumerClient: EventHubConsumerClient; +// let startPosition: { [partitionId: string]: EventPosition }; + +// before("validate environment", function (): void { +// should.exist( +// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." +// ); +// should.exist( +// env[EnvVarKeys.EVENTHUB_NAME], +// "define EVENTHUB_NAME in your environment before running integration tests." +// ); +// }); + +// beforeEach(async () => { +// debug("Creating the clients.."); +// producerClient = new EventHubProducerClient(service.connectionString, service.path); +// consumerClient = new EventHubConsumerClient( +// EventHubConsumerClient.defaultConsumerGroupName, +// service.connectionString, +// service.path +// ); +// startPosition = await getStartingPositionsForTests(consumerClient); +// }); + +// afterEach(async () => { +// debug("Closing the clients.."); +// await producerClient.close(); +// await consumerClient.close(); +// }); + +// describe("Create batch", function (): void { +// describe("tryAdd", function () { +// it("doesn't grow if invalid events are added", async () => { +// const batch = await producerClient.createBatch({ maxSizeInBytes: 20 }); +// const event = { body: Buffer.alloc(30).toString() }; + +// const numToAdd = 5; +// let failures = 0; +// for (let i = 0; i < numToAdd; i++) { +// if (!batch.tryAdd(event)) { +// failures++; +// } +// } + +// failures.should.equal(5); +// batch.sizeInBytes.should.equal(0); +// }); +// }); + +// it("partitionId is set as expected", async () => { +// const batch = await producerClient.createBatch({ +// partitionId: "0", +// }); +// should.equal(batch.partitionId, "0"); +// }); + +// it("partitionId is set as expected when it is 0 i.e. falsy", async () => { +// const batch = await producerClient.createBatch({ +// // @ts-expect-error Testing the value 0 is not ignored. +// partitionId: 0, +// }); +// should.equal(batch.partitionId, "0"); +// }); + +// it("partitionKey is set as expected", async () => { +// const batch = await producerClient.createBatch({ +// partitionKey: "boo", +// }); +// should.equal(batch.partitionKey, "boo"); +// }); + +// it("partitionKey is set as expected when it is 0 i.e. falsy", async () => { +// const batch = await producerClient.createBatch({ +// // @ts-expect-error Testing the value 0 is not ignored. +// partitionKey: 0, +// }); +// should.equal(batch.partitionKey, "0"); +// }); + +// it("maxSizeInBytes is set as expected", async () => { +// const batch = await producerClient.createBatch({ maxSizeInBytes: 30 }); +// should.equal(batch.maxSizeInBytes, 30); +// }); + +// it("should be sent successfully", async function (): Promise { +// const list = ["Albert", `${Buffer.from("Mike".repeat(1300000))}`, "Marie"]; + +// const batch = await producerClient.createBatch({ +// partitionId: "0", +// }); + +// batch.partitionId!.should.equal("0"); +// should.not.exist(batch.partitionKey); +// batch.maxSizeInBytes.should.be.gt(0); + +// should.equal(batch.tryAdd({ body: list[0] }), true); +// should.equal(batch.tryAdd({ body: list[1] }), false); // The Mike message will be rejected - it's over the limit. +// should.equal(batch.tryAdd({ body: list[2] }), true); // Marie should get added"; + +// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( +// producerClient +// ); + +// const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { +// startPosition, +// }); +// await producerClient.sendBatch(batch); + +// let receivedEvents; + +// try { +// receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); +// } finally { +// await subscriber.close(); +// } + +// // Mike didn't make it - the message was too big for the batch +// // and was rejected above. +// [list[0], list[2]].should.be.deep.eq( +// receivedEvents.map((event) => event.body), +// "Received messages should be equal to our sent messages" +// ); +// }); + +// it("should be sent successfully when partitionId is 0 i.e. falsy", async function (): Promise { +// const list = ["Albert", "Marie"]; + +// const batch = await producerClient.createBatch({ +// // @ts-expect-error Testing the value 0 is not ignored. +// partitionId: 0, +// }); + +// batch.partitionId!.should.equal("0"); +// should.not.exist(batch.partitionKey); +// batch.maxSizeInBytes.should.be.gt(0); + +// should.equal(batch.tryAdd({ body: list[0] }), true); +// should.equal(batch.tryAdd({ body: list[1] }), true); + +// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( +// producerClient +// ); + +// const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { +// startPosition, +// }); +// await producerClient.sendBatch(batch); + +// let receivedEvents; + +// try { +// receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); +// } finally { +// await subscriber.close(); +// } + +// list.should.be.deep.eq( +// receivedEvents.map((event) => event.body), +// "Received messages should be equal to our sent messages" +// ); +// }); + +// it("should be sent successfully when partitionKey is 0 i.e. falsy", async function (): Promise { +// const list = ["Albert", "Marie"]; + +// const batch = await producerClient.createBatch({ +// // @ts-expect-error Testing the value 0 is not ignored. +// partitionKey: 0, +// }); + +// batch.partitionKey!.should.equal("0"); +// should.not.exist(batch.partitionId); +// batch.maxSizeInBytes.should.be.gt(0); + +// should.equal(batch.tryAdd({ body: list[0] }), true); +// should.equal(batch.tryAdd({ body: list[1] }), true); + +// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( +// producerClient +// ); + +// const subscriber = consumerClient.subscribe(subscriptionEventHandler, { +// startPosition, +// }); +// await producerClient.sendBatch(batch); + +// let receivedEvents; +// const allPartitionIds = await producerClient.getPartitionIds(); +// try { +// receivedEvents = await subscriptionEventHandler.waitForEvents(allPartitionIds, 2); +// } finally { +// await subscriber.close(); +// } + +// list.should.be.deep.eq( +// receivedEvents.map((event) => event.body), +// "Received messages should be equal to our sent messages" +// ); +// }); + +// it("should be sent successfully with properties", async function (): Promise { +// const properties = { test: "super" }; +// const list = [ +// { body: "Albert-With-Properties", properties }, +// { body: "Mike-With-Properties", properties }, +// { body: "Marie-With-Properties", properties }, +// ]; + +// const batch = await producerClient.createBatch({ +// partitionId: "0", +// }); + +// batch.maxSizeInBytes.should.be.gt(0); + +// should.equal(batch.tryAdd(list[0]), true); +// should.equal(batch.tryAdd(list[1]), true); +// should.equal(batch.tryAdd(list[2]), true); + +// const receivedEvents: ReceivedEventData[] = []; +// let waitUntilEventsReceivedResolver: (value?: any) => void; +// const waitUntilEventsReceived = new Promise( +// (resolve) => (waitUntilEventsReceivedResolver = resolve) +// ); + +// const sequenceNumber = (await consumerClient.getPartitionProperties("0")) +// .lastEnqueuedSequenceNumber; + +// const subscriber = consumerClient.subscribe( +// "0", +// { +// async processError() { +// /* no-op */ +// }, +// async processEvents(events) { +// receivedEvents.push(...events); +// if (receivedEvents.length >= 3) { +// waitUntilEventsReceivedResolver(); +// } +// }, +// }, +// { +// startPosition: { +// sequenceNumber, +// }, +// maxBatchSize: 3, +// } +// ); + +// await producerClient.sendBatch(batch); +// await waitUntilEventsReceived; +// await subscriber.close(); + +// sequenceNumber.should.be.lessThan(receivedEvents[0].sequenceNumber); +// sequenceNumber.should.be.lessThan(receivedEvents[1].sequenceNumber); +// sequenceNumber.should.be.lessThan(receivedEvents[2].sequenceNumber); + +// [list[0], list[1], list[2]].should.be.deep.eq( +// receivedEvents.map((event) => { +// return { +// body: event.body, +// properties: event.properties, +// }; +// }), +// "Received messages should be equal to our sent messages" +// ); +// }); + +// 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", +// }); + +// for (let i = 0; i < 2; i++) { +// eventDataBatch.tryAdd( +// { body: `${list[i].name}` }, +// { +// tracingOptions: { +// tracingContext: setSpan(context.active(), 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: [], +// }, +// { +// 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("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) { +// eventDataBatch.tryAdd(event); +// } + +// should.equal(eventDataBatch.count, 2, "Unexpected number of events in batch."); +// should.equal( +// eventDataBatch["_messageSpanContexts"].length, +// 0, +// "Unexpected number of span contexts in batch." +// ); +// }); + +// function legacyOptionsUsingSpanContext( +// rootSpan: TestSpan +// ): Pick { +// return { +// parentSpan: rootSpan.spanContext(), +// }; +// } + +// function legacyOptionsUsingSpan(rootSpan: TestSpan): Pick { +// return { +// parentSpan: rootSpan, +// }; +// } + +// function modernOptions(rootSpan: TestSpan): OperationOptions { +// return { +// tracingOptions: { +// tracingContext: setSpan(context.active(), rootSpan), +// }, +// }; +// } + +// [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" }]; + +// 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("with partition key should be sent successfully.", async function (): Promise { +// const eventDataBatch = await producerClient.createBatch({ partitionKey: "1" }); +// for (let i = 0; i < 5; i++) { +// eventDataBatch.tryAdd({ body: `Hello World ${i}` }); +// } +// await producerClient.sendBatch(eventDataBatch); +// }); + +// it("with max message size should be sent successfully.", async function (): Promise { +// const eventDataBatch = await producerClient.createBatch({ +// maxSizeInBytes: 5000, +// partitionId: "0", +// }); +// const message = { body: `${Buffer.from("Z".repeat(4096))}` }; +// for (let i = 1; i <= 3; i++) { +// const isAdded = eventDataBatch.tryAdd(message); +// if (!isAdded) { +// debug(`Unable to add ${i} event to the batch`); +// break; +// } +// } +// await producerClient.sendBatch(eventDataBatch); +// eventDataBatch.count.should.equal(1); +// }); +// }); + +// describe("Multiple sendBatch calls", function (): void { +// it("should be sent successfully in parallel", async function (): Promise { +// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( +// consumerClient +// ); + +// const promises = []; +// for (let i = 0; i < 5; i++) { +// promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); +// } +// await Promise.all(promises); + +// const subscription = await consumerClient.subscribe(subscriptionEventHandler, { +// startPosition, +// }); + +// try { +// const events = await subscriptionEventHandler.waitForEvents( +// await consumerClient.getPartitionIds({}), +// 5 +// ); + +// // we've allowed the server to choose which partition the messages are distributed to +// // so our expectation here is just that all the bodies have arrived +// const bodiesOnly = events.map((evt) => evt.body); +// bodiesOnly.sort(); + +// bodiesOnly.should.deep.equal([ +// "Hello World 0", +// "Hello World 1", +// "Hello World 2", +// "Hello World 3", +// "Hello World 4", +// ]); +// } finally { +// subscription.close(); +// } +// }); + +// it("should be sent successfully in parallel, even when exceeding max event listener count of 1000", async function (): Promise { +// const senderCount = 1200; +// try { +// const promises = []; +// for (let i = 0; i < senderCount; i++) { +// promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); +// } +// await Promise.all(promises); +// } catch (err) { +// debug("An error occurred while running the test: ", err); +// throw err; +// } +// }); + +// it("should be sent successfully in parallel by multiple clients", async function (): Promise { +// const senderCount = 3; +// try { +// const promises = []; +// for (let i = 0; i < senderCount; i++) { +// if (i === 0) { +// debug(">>>>> Sending a message to partition %d", i); +// promises.push( +// await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "0" }) +// ); +// } else if (i === 1) { +// debug(">>>>> Sending a message to partition %d", i); +// promises.push( +// await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "1" }) +// ); +// } else { +// debug(">>>>> Sending a message to the hub when i == %d", i); +// promises.push(await producerClient.sendBatch([{ body: `Hello World ${i}` }])); +// } +// } +// await Promise.all(promises); +// } catch (err) { +// debug("An error occurred while running the test: ", err); +// throw err; +// } +// }); + +// it("should fail when a message greater than 1 MB is sent and succeed when a normal message is sent after that on the same link.", async function (): Promise { +// const data: EventData = { +// body: Buffer.from("Z".repeat(1300000)), +// }; +// try { +// debug("Sending a message of 300KB..."); +// await producerClient.sendBatch([data], { partitionId: "0" }); +// 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 +// ); +// } +// await producerClient.sendBatch([{ body: "Hello World EventHub!!" }], { partitionId: "0" }); +// 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"); + +// 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: [ +// { +// 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: [], +// }, +// ], +// }, +// ], +// }; + +// 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); +// } +// }); + +// it("can be manually traced", async function (): Promise { +// const { tracer, resetTracer } = setTracerForTest(); + +// const rootSpan = tracer.startSpan("root"); + +// 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), +// }, +// }); +// 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.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."); + +// 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(); +// }); + +// 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, { +// 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(); +// }); + +// 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." +// ); +// } +// }); +// }); + +// 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( +// "partitionId and partitionKey cannot both be set when creating a batch" +// ); +// } +// }); + +// 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( +// "partitionId and partitionKey cannot both be set when creating a batch" +// ); +// } +// }); + +// 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( +// "partitionId and partitionKey cannot both be set when creating a batch" +// ); +// } +// }); + +// 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 +// ); +// } +// }); +// }); +// 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"); +// } +// }); +// }); + +// 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("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 +// ); +// } +// }); +// }); +// }); +// }); +// }).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..7abe624f9625 100644 --- a/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts +++ b/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts @@ -1,282 +1,282 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { EnvVarKeys, getEnvVars, setTracerForTest } from "./utils/testUtils"; -import { - EventHubBufferedProducerClient, - EventHubConsumerClient, - EventHubProducerClient, - MessagingError, -} from "../../src"; -import { context, setSpan } from "@azure/core-tracing"; -import { SpanGraph } from "@azure/test-utils"; -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import { createMockServer } from "./utils/mockService"; -import debugModule from "debug"; -import { testWithServiceTypes } from "./utils/testWithServiceTypes"; - -const should = chai.should(); -chai.use(chaiAsPromised); -const debug = debugModule("azure:event-hubs:hubruntime-spec"); - -type ClientCommonMethods = Pick< - EventHubProducerClient, - "close" | "getEventHubProperties" | "getPartitionIds" | "getPartitionProperties" ->; - -testWithServiceTypes((serviceVersion) => { - const env = getEnvVars(); - if (serviceVersion === "mock") { - let service: ReturnType; - before("Starting mock service", () => { - service = createMockServer(); - return service.start(); - }); - - after("Stopping mock service", () => { - return service?.stop(); - }); - } - - describe("RuntimeInformation", function (): void { - const clientTypes = [ - "EventHubBufferedProducerClient", - "EventHubConsumerClient", - "EventHubProducerClient", - ] as const; - const clientMap = new Map(); - - const service = { - connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - path: env[EnvVarKeys.EVENTHUB_NAME], - }; - before("validate environment", function (): void { - should.exist( - env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], - "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." - ); - should.exist( - env[EnvVarKeys.EVENTHUB_NAME], - "define EVENTHUB_NAME in your environment before running integration tests." - ); - }); - - beforeEach(async () => { - debug("Creating the clients.."); - clientMap.set( - "EventHubBufferedProducerClient", - new EventHubBufferedProducerClient(service.connectionString, service.path) - ); - clientMap.set( - "EventHubConsumerClient", - new EventHubConsumerClient( - EventHubConsumerClient.defaultConsumerGroupName, - service.connectionString, - service.path - ) - ); - clientMap.set( - "EventHubProducerClient", - new EventHubProducerClient(service.connectionString, service.path) - ); - }); - - afterEach("close the connection", async function (): Promise { - for (const client of clientMap.values()) { - await client?.close(); - } - }); - - function arrayOfIncreasingNumbersFromZero(length: any): Array { - const result = new Array(length); - for (let i = 0; i < length; i++) { - result[i] = `${i}`; - } - return result; - } - - clientTypes.forEach((clientType) => { - describe(`${clientType}.getPartitionIds`, () => { - it("returns an array of partition ids", async () => { - const client = clientMap.get(clientType)!; - const ids = await client.getPartitionIds({}); - ids.should.have.members(arrayOfIncreasingNumbersFromZero(ids.length)); - }); - - 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(); - }); - }); - - describe(`${clientType}.getEventHubProperties`, () => { - it("gets the Event Hub runtime information", async () => { - const client = clientMap.get(clientType)!; - const hubRuntimeInfo = await client.getEventHubProperties(); - hubRuntimeInfo.name.should.equal(service.path); - - hubRuntimeInfo.partitionIds.should.have.members( - arrayOfIncreasingNumbersFromZero(hubRuntimeInfo.partitionIds.length) - ); - hubRuntimeInfo.createdOn.should.be.instanceof(Date); - }); - - 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) - ); - 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(); - }); - }); - - describe(`${clientType}.getPartitionProperties`, () => { - it("should throw an error if partitionId is missing", async () => { - try { - const client = clientMap.get(clientType)!; - await client.getPartitionProperties(undefined as any); - throw new Error("Test failure"); - } catch (err) { - (err as any).name.should.equal("TypeError"); - (err as any).message.should.equal( - `getPartitionProperties called without required argument "partitionId"` - ); - } - }); - - it("gets the partition runtime information with partitionId as a string", async () => { - const client = clientMap.get(clientType)!; - const partitionRuntimeInfo = await client.getPartitionProperties("0"); - 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); - }); - - it("gets the partition runtime information with partitionId as a number", async () => { - const client = clientMap.get(clientType)!; - const partitionRuntimeInfo = await client.getPartitionProperties(0 as any); - 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); - }); - - it("bubbles up error from service for invalid partitionId", async () => { - try { - const client = clientMap.get(clientType)!; - await client.getPartitionProperties("boo"); - throw new Error("Test failure"); - } catch (err) { - should.exist(err); - should.equal((err as MessagingError).code, "ArgumentOutOfRangeError"); - } - }); - - 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(); - }); - }); - }); - }).timeout(60000); -}); +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT license. + +// import { EnvVarKeys, getEnvVars, setTracerForTest } from "./utils/testUtils"; +// import { +// EventHubBufferedProducerClient, +// EventHubConsumerClient, +// EventHubProducerClient, +// MessagingError, +// } from "../../src"; +// import { context, setSpan } from "@azure/core-tracing"; +// import { SpanGraph } from "@azure/test-utils"; +// import chai from "chai"; +// import chaiAsPromised from "chai-as-promised"; +// import { createMockServer } from "./utils/mockService"; +// import debugModule from "debug"; +// import { testWithServiceTypes } from "./utils/testWithServiceTypes"; + +// const should = chai.should(); +// chai.use(chaiAsPromised); +// const debug = debugModule("azure:event-hubs:hubruntime-spec"); + +// type ClientCommonMethods = Pick< +// EventHubProducerClient, +// "close" | "getEventHubProperties" | "getPartitionIds" | "getPartitionProperties" +// >; + +// testWithServiceTypes((serviceVersion) => { +// const env = getEnvVars(); +// if (serviceVersion === "mock") { +// let service: ReturnType; +// before("Starting mock service", () => { +// service = createMockServer(); +// return service.start(); +// }); + +// after("Stopping mock service", () => { +// return service?.stop(); +// }); +// } + +// describe("RuntimeInformation", function (): void { +// const clientTypes = [ +// "EventHubBufferedProducerClient", +// "EventHubConsumerClient", +// "EventHubProducerClient", +// ] as const; +// const clientMap = new Map(); + +// const service = { +// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// path: env[EnvVarKeys.EVENTHUB_NAME], +// }; +// before("validate environment", function (): void { +// should.exist( +// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], +// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." +// ); +// should.exist( +// env[EnvVarKeys.EVENTHUB_NAME], +// "define EVENTHUB_NAME in your environment before running integration tests." +// ); +// }); + +// beforeEach(async () => { +// debug("Creating the clients.."); +// clientMap.set( +// "EventHubBufferedProducerClient", +// new EventHubBufferedProducerClient(service.connectionString, service.path) +// ); +// clientMap.set( +// "EventHubConsumerClient", +// new EventHubConsumerClient( +// EventHubConsumerClient.defaultConsumerGroupName, +// service.connectionString, +// service.path +// ) +// ); +// clientMap.set( +// "EventHubProducerClient", +// new EventHubProducerClient(service.connectionString, service.path) +// ); +// }); + +// afterEach("close the connection", async function (): Promise { +// for (const client of clientMap.values()) { +// await client?.close(); +// } +// }); + +// function arrayOfIncreasingNumbersFromZero(length: any): Array { +// const result = new Array(length); +// for (let i = 0; i < length; i++) { +// result[i] = `${i}`; +// } +// return result; +// } + +// clientTypes.forEach((clientType) => { +// describe(`${clientType}.getPartitionIds`, () => { +// it("returns an array of partition ids", async () => { +// const client = clientMap.get(clientType)!; +// const ids = await client.getPartitionIds({}); +// ids.should.have.members(arrayOfIncreasingNumbersFromZero(ids.length)); +// }); + +// 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(); +// }); +// }); + +// describe(`${clientType}.getEventHubProperties`, () => { +// it("gets the Event Hub runtime information", async () => { +// const client = clientMap.get(clientType)!; +// const hubRuntimeInfo = await client.getEventHubProperties(); +// hubRuntimeInfo.name.should.equal(service.path); + +// hubRuntimeInfo.partitionIds.should.have.members( +// arrayOfIncreasingNumbersFromZero(hubRuntimeInfo.partitionIds.length) +// ); +// hubRuntimeInfo.createdOn.should.be.instanceof(Date); +// }); + +// 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) +// ); +// 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(); +// }); +// }); + +// describe(`${clientType}.getPartitionProperties`, () => { +// it("should throw an error if partitionId is missing", async () => { +// try { +// const client = clientMap.get(clientType)!; +// await client.getPartitionProperties(undefined as any); +// throw new Error("Test failure"); +// } catch (err) { +// (err as any).name.should.equal("TypeError"); +// (err as any).message.should.equal( +// `getPartitionProperties called without required argument "partitionId"` +// ); +// } +// }); + +// it("gets the partition runtime information with partitionId as a string", async () => { +// const client = clientMap.get(clientType)!; +// const partitionRuntimeInfo = await client.getPartitionProperties("0"); +// 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); +// }); + +// it("gets the partition runtime information with partitionId as a number", async () => { +// const client = clientMap.get(clientType)!; +// const partitionRuntimeInfo = await client.getPartitionProperties(0 as any); +// 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); +// }); + +// it("bubbles up error from service for invalid partitionId", async () => { +// try { +// const client = clientMap.get(clientType)!; +// await client.getPartitionProperties("boo"); +// throw new Error("Test failure"); +// } catch (err) { +// should.exist(err); +// should.equal((err as MessagingError).code, "ArgumentOutOfRangeError"); +// } +// }); + +// 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(); +// }); +// }); +// }); +// }).timeout(60000); +// }); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts index 8cd86717849d..7df341abdc51 100644 --- a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts @@ -49,17 +49,29 @@ export class OpenTelemetryInstrumenter implements Instrumenter { ); } +<<<<<<< HEAD parseTraceparentHeader(traceparentHeader: string): TracingContext { return propagator.extract( context.active(), { traceparent: traceparentHeader }, defaultTextMapGetter ); +======= + parseTraceparentHeader(traceparentHeader: string): TracingSpanContext | undefined { + const newContext = propagation.extract(context.active(), { traceparent: traceparentHeader }); + console.log("parseTraceparentHeader", trace.getSpanContext(newContext), traceparentHeader); + return trace.getSpanContext(newContext); +>>>>>>> 130571e16... wip } createRequestHeaders(tracingContext?: TracingContext): Record { const headers: Record = {}; +<<<<<<< HEAD propagator.inject(tracingContext || context.active(), headers, defaultTextMapSetter); +======= + propagation.inject(tracingContext || context.active(), headers); + console.log("createRequestHeaders", headers); +>>>>>>> 130571e16... wip return headers; } } diff --git a/sdk/keyvault/keyvault-common/src/tracingHelpers.ts b/sdk/keyvault/keyvault-common/src/tracingHelpers.ts index 60404941aaf2..0dc3a6415817 100644 --- a/sdk/keyvault/keyvault-common/src/tracingHelpers.ts +++ b/sdk/keyvault/keyvault-common/src/tracingHelpers.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Span, SpanStatusCode, createSpanFunction } from "@azure/core-tracing"; +// import { Span, SpanStatusCode, createSpanFunction } from "@azure/core-tracing"; import { OperationOptions } from "@azure/core-http"; /** @@ -19,7 +19,7 @@ export interface TracedFunction { ( operationName: string, options: TOptions, - cb: (options: TOptions, span: Span) => Promise + cb: (options: TOptions, span: any) => Promise ): Promise; } @@ -32,33 +32,34 @@ export interface TracedFunction { * * @internal */ -export function createTraceFunction(prefix: string): TracedFunction { - const createSpan = createSpanFunction({ - namespace: "Microsoft.KeyVault", - packagePrefix: prefix, - }); +export function createTraceFunction(_prefix: string): TracedFunction { + // const createSpan = createSpanFunction({ + // namespace: "Microsoft.KeyVault", + // packagePrefix: prefix, + // }); - return async function (operationName, options, cb) { - const { updatedOptions, span } = createSpan(operationName, options); + return async function (..._args: any[]) { + throw new Error("why are we using this still?"); + // const { updatedOptions, span } = createSpan(operationName, options); - try { - // NOTE: we really do need to await on this function here so we can handle any exceptions thrown and properly - // close the span. - const result = await cb(updatedOptions, span); + // try { + // // NOTE: we really do need to await on this function here so we can handle any exceptions thrown and properly + // // close the span. + // const result = await cb(updatedOptions, span); - // otel 0.16+ needs this or else the code ends up being set as UNSET - span.setStatus({ - code: SpanStatusCode.OK, - }); - return result; - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, - }); - throw err; - } finally { - span.end(); - } + // // otel 0.16+ needs this or else the code ends up being set as UNSET + // span.setStatus({ + // code: SpanStatusCode.OK, + // }); + // return result; + // } catch (err) { + // span.setStatus({ + // code: SpanStatusCode.ERROR, + // message: err.message, + // }); + // throw err; + // } finally { + // span.end(); + // } }; } diff --git a/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts b/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts index a4110b3af896..f7d1a492fc59 100644 --- a/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts +++ b/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts @@ -1,38 +1,7 @@ -import { setSpan, context as otContext, OperationTracingOptions } from "@azure/core-tracing"; -import { setTracer } from "@azure/test-utils"; -import { assert } from "chai"; +// import { setSpan, context as otContext, OperationTracingOptions } from "@azure/core-tracing"; +// import { setTracer } from "@azure/test-utils"; +// import { assert } from "chai"; -const prefix = "Azure.KeyVault"; +// const prefix = "Azure.KeyVault"; -export async function supportsTracing( - callback: (tracingOptions: OperationTracingOptions) => Promise, - children: string[] -): Promise { - const tracer = setTracer(); - const rootSpan = tracer.startSpan("root"); - const tracingContext = setSpan(otContext.active(), rootSpan); - - try { - await callback({ tracingContext }); - } finally { - rootSpan.end(); - } - - // Ensure any spans created by KeyVault are parented correctly - let rootSpans = tracer - .getRootSpans() - .filter((span) => span.name.startsWith(prefix) || span.name === "root"); - - assert.equal(rootSpans.length, 1, "Should only have one root span."); - assert.strictEqual(rootSpan, rootSpans[0], "The root span should match what was passed in."); - - // Ensure top-level children are created correctly. - // Testing the entire tree structure can be tricky as other packages might create their own spans. - const spanGraph = tracer.getSpanGraph(rootSpan.spanContext().traceId); - const directChildren = spanGraph.roots[0].children.map((child) => child.name); - // LROs might poll N times, so we'll make a unique array and compare that. - assert.sameMembers(Array.from(new Set(directChildren)), children); - - // Ensure all spans are properly closed - assert.equal(tracer.getActiveSpans().length, 0, "All spans should have had end called"); -} +export async function supportsTracing(..._args: any[]): Promise {} diff --git a/sdk/keyvault/keyvault-secrets/package.json b/sdk/keyvault/keyvault-secrets/package.json index d156b52d3cb5..1aa815ca843a 100644 --- a/sdk/keyvault/keyvault-secrets/package.json +++ b/sdk/keyvault/keyvault-secrets/package.json @@ -105,7 +105,7 @@ "@azure/core-http": "^2.0.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-tracing": "1.0.0-preview.14", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" }, diff --git a/sdk/keyvault/keyvault-secrets/src/index.ts b/sdk/keyvault/keyvault-secrets/src/index.ts index 4ac15e5414a7..34fa3a186449 100644 --- a/sdk/keyvault/keyvault-secrets/src/index.ts +++ b/sdk/keyvault/keyvault-secrets/src/index.ts @@ -52,7 +52,7 @@ import { } from "./secretsModels"; import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "./identifier"; import { getSecretFromSecretBundle } from "./transformations"; -import { createTraceFunction } from "../../keyvault-common/src"; +import { createTracingClient, TracingClient } from "@azure/core-tracing"; export { SecretClientOptions, @@ -84,8 +84,6 @@ export { logger, }; -const withTrace = createTraceFunction("Azure.KeyVault.Secrets.SecretClient"); - /** * The SecretClient provides methods to manage {@link KeyVaultSecret} in * the Azure Key Vault. The client supports creating, retrieving, updating, @@ -104,6 +102,8 @@ export class SecretClient { */ private readonly client: KeyVaultClient; + private readonly tracingClient: TracingClient; + /** * Creates an instance of SecretClient. * @@ -156,6 +156,12 @@ export class SecretClient { }, }; + this.tracingClient = createTracingClient({ + namespace: "Microsoft.KeyVault", + packageName: "@azure/keyvault-secrets", + packageVersion: SDK_VERSION, + }); + this.client = new KeyVaultClient( pipelineOptions.serviceVersion || LATEST_API_VERSION, createPipelineFromOptions(internalPipelineOptions, authPolicy) @@ -177,7 +183,7 @@ export class SecretClient { * @param value - The value of the secret. * @param options - The optional parameters. */ - public setSecret( + public async setSecret( secretName: string, value: string, options: SetSecretOptions = {} @@ -195,7 +201,7 @@ export class SecretClient { }, }; } - return withTrace("setSecret", unflattenedOptions, async (updatedOptions) => { + return this.tracingClient.withSpan("setSecret", unflattenedOptions, async (updatedOptions) => { const response = await this.client.setSecret( this.vaultUrl, secretName, @@ -284,15 +290,19 @@ export class SecretClient { }; } - return withTrace("updateSecretProperties", unflattenedOptions, async (updatedOptions) => { - const response = await this.client.updateSecret( - this.vaultUrl, - secretName, - secretVersion, - updatedOptions - ); - return getSecretFromSecretBundle(response).properties; - }); + return this.tracingClient.withSpan( + "updateSecretProperties", + unflattenedOptions, + async (updatedOptions) => { + const response = await this.client.updateSecret( + this.vaultUrl, + secretName, + secretVersion, + updatedOptions + ); + return getSecretFromSecretBundle(response).properties; + } + ); } /** @@ -308,8 +318,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public getSecret(secretName: string, options: GetSecretOptions = {}): Promise { - return withTrace("getSecret", options, async (updatedOptions) => { + public async getSecret( + secretName: string, + options: GetSecretOptions = {} + ): Promise { + return this.tracingClient.withSpan("getSecret", options, async (updatedOptions) => { const response = await this.client.getSecret( this.vaultUrl, secretName, @@ -333,11 +346,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public getDeletedSecret( + public async getDeletedSecret( secretName: string, options: GetDeletedSecretOptions = {} ): Promise { - return withTrace("getDeletedSecret", options, async (updatedOptions) => { + return this.tracingClient.withSpan("getDeletedSecret", options, async (updatedOptions) => { const response = await this.client.getDeletedSecret( this.vaultUrl, secretName, @@ -363,11 +376,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public purgeDeletedSecret( + public async purgeDeletedSecret( secretName: string, options: PurgeDeletedSecretOptions = {} ): Promise { - return withTrace("purgeDeletedSecret", options, async (updatedOptions) => { + return this.tracingClient.withSpan("purgeDeletedSecret", options, async (updatedOptions) => { await this.client.purgeDeletedSecret(this.vaultUrl, secretName, updatedOptions); }); } @@ -432,11 +445,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public backupSecret( + public async backupSecret( secretName: string, options: BackupSecretOptions = {} ): Promise { - return withTrace("backupSecret", options, async (updatedOptions) => { + return this.tracingClient.withSpan("backupSecret", options, async (updatedOptions) => { const response = await this.client.backupSecret(this.vaultUrl, secretName, updatedOptions); return response.value; @@ -458,11 +471,11 @@ export class SecretClient { * @param secretBundleBackup - The backup blob associated with a secret bundle. * @param options - The optional parameters. */ - public restoreSecretBackup( + public async restoreSecretBackup( secretBundleBackup: Uint8Array, options: RestoreSecretBackupOptions = {} ): Promise { - return withTrace("restoreSecretBackup", options, async (updatedOptions) => { + return this.tracingClient.withSpan("restoreSecretBackup", options, async (updatedOptions) => { const response = await this.client.restoreSecret( this.vaultUrl, secretBundleBackup, @@ -488,7 +501,7 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await withTrace( + const currentSetResponse = await this.tracingClient.withSpan( "listPropertiesOfSecretVersions", optionsComplete, (updatedOptions) => this.client.getSecretVersions(this.vaultUrl, secretName, updatedOptions) @@ -502,7 +515,7 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await withTrace( + const currentSetResponse = await this.tracingClient.withSpan( "listPropertiesOfSecretVersions", options, (updatedOptions) => @@ -589,8 +602,8 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await withTrace( - "listPropertiesOfSecrets", + const currentSetResponse = await this.tracingClient.withSpan( + "listPropertiesOfSecretsPage", optionsComplete, (updatedOptions) => this.client.getSecrets(this.vaultUrl, updatedOptions) ); @@ -603,8 +616,8 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await withTrace( - "listPropertiesOfSecrets", + const currentSetResponse = await this.tracingClient.withSpan( + "listPropertiesOfSecretsPage", options, (updatedOptions) => this.client.getSecrets(continuationState.continuationToken!, updatedOptions) @@ -682,7 +695,7 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await withTrace( + const currentSetResponse = await this.tracingClient.withSpan( "listDeletedSecrets", optionsComplete, (updatedOptions) => this.client.getDeletedSecrets(this.vaultUrl, updatedOptions) @@ -695,8 +708,11 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await withTrace("lisDeletedSecrets", options, (updatedOptions) => - this.client.getDeletedSecrets(continuationState.continuationToken!, updatedOptions) + const currentSetResponse = await this.tracingClient.withSpan( + "lisDeletedSecrets", + options, + (updatedOptions) => + this.client.getDeletedSecrets(continuationState.continuationToken!, updatedOptions) ); continuationState.continuationToken = currentSetResponse.nextLink; if (currentSetResponse.value) { diff --git a/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts b/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts index 7c5746e583ae..1affe3fd190d 100644 --- a/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts +++ b/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts @@ -10,12 +10,15 @@ import { import { KeyVaultClient } from "../../generated/keyVaultClient"; import { getSecretFromSecretBundle } from "../../transformations"; import { OperationOptions } from "@azure/core-http"; -import { createTraceFunction } from "../../../../keyvault-common/src"; +import { createTracingClient } from "@azure/core-tracing"; /** * @internal */ -const withTrace = createTraceFunction("Azure.KeyVault.Secrets.DeleteSecretPoller"); +const withTrace = createTracingClient({ + namespace: "Microsoft.KeyVault.Delete", + packageName: "@azure/keyvault-secrets", +}).withSpan; /** * An interface representing the state of a delete secret's poll operation @@ -43,7 +46,10 @@ export class DeleteSecretPollOperation extends KeyVaultSecretPollOperation< * Sends a delete request for the given Key Vault Key's name to the Key Vault service. * Since the Key Vault Key won't be immediately deleted, we have {@link beginDeleteKey}. */ - private deleteSecret(name: string, options: DeleteSecretOptions = {}): Promise { + private async deleteSecret( + name: string, + options: DeleteSecretOptions = {} + ): Promise { return withTrace("deleteSecret", options, async (updatedOptions) => { const response = await this.client.deleteSecret(this.vaultUrl, name, updatedOptions); return getSecretFromSecretBundle(response); @@ -54,7 +60,7 @@ export class DeleteSecretPollOperation extends KeyVaultSecretPollOperation< * The getDeletedSecret method returns the specified deleted secret along with its properties. * This operation requires the secrets/get permission. */ - private getDeletedSecret( + private async getDeletedSecret( name: string, options: GetDeletedSecretOptions = {} ): Promise { diff --git a/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts b/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts index 96cfdc6f6332..257a21eb8efb 100644 --- a/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts +++ b/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts @@ -15,13 +15,17 @@ import { import { KeyVaultClient } from "../../generated/keyVaultClient"; import { getSecretFromSecretBundle } from "../../transformations"; import { OperationOptions } from "@azure/core-http"; +import { createTracingClient } from "@azure/core-tracing"; -import { createTraceFunction } from "../../../../keyvault-common/src"; +// import { createTraceFunction } from "../../../../keyvault-common/src"; /** * @internal */ -const withTrace = createTraceFunction("Azure.KeyVault.Secrets.RecoverDeletedSecretPoller"); +const withTrace = createTracingClient({ + namespace: "Microsoft.KeyVault.Recover", + packageName: "@azure/keyvault-secrets", +}).withSpan; /** * An interface representing the state of a delete secret's poll operation @@ -49,7 +53,7 @@ export class RecoverDeletedSecretPollOperation extends KeyVaultSecretPollOperati * The getSecret method returns the specified secret along with its properties. * This operation requires the secrets/get permission. */ - private getSecret(name: string, options: GetSecretOptions = {}): Promise { + private async getSecret(name: string, options: GetSecretOptions = {}): Promise { return withTrace("getSecret", options, async (updatedOptions) => { const response = await this.client.getSecret( this.vaultUrl, @@ -65,7 +69,7 @@ export class RecoverDeletedSecretPollOperation extends KeyVaultSecretPollOperati * The recoverDeletedSecret method recovers the specified deleted secret along with its properties. * This operation requires the secrets/recover permission. */ - private recoverDeletedSecret( + private async recoverDeletedSecret( name: string, options: GetSecretOptions = {} ): Promise { diff --git a/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts b/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts index 54755de842d2..32828e3d233d 100644 --- a/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts +++ b/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts @@ -2,8 +2,9 @@ // Licensed under the MIT license. import { Context } from "mocha"; -import { assert } from "chai"; -import { supportsTracing } from "../../../keyvault-common/test/utils/supportsTracing"; +import chai, { assert } from "chai"; +import { chaiAzureTrace } from "@azure/test-utils"; +chai.use(chaiAzureTrace); import { env, Recorder } from "@azure-tools/test-recorder"; import { AbortController } from "@azure/abort-controller"; @@ -371,8 +372,8 @@ describe("Secret client - create, read, update and delete operations", () => { const secretName = testClient.formatName( `${secretPrefix}-${this!.test!.title}-${secretSuffix}` ); - await supportsTracing( - (tracingOptions) => client.setSecret(secretName, "value", { tracingOptions }), + await assert.supportsTracing( + (options) => client.setSecret(secretName, "value", options), ["Azure.KeyVault.Secrets.SecretClient.setSecret"] ); }); From ab1c76977aa085bb77f73ac7a24f7942e4e8c871 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Wed, 12 Jan 2022 14:28:43 -0800 Subject: [PATCH 5/7] changelog for core-tracing --- sdk/core/core-tracing/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/core/core-tracing/CHANGELOG.md b/sdk/core/core-tracing/CHANGELOG.md index 43a39aa57f24..1c76921c21dd 100644 --- a/sdk/core/core-tracing/CHANGELOG.md +++ b/sdk/core/core-tracing/CHANGELOG.md @@ -6,8 +6,11 @@ ### Breaking Changes -- SpanOptions has been removed from OperationTracingOptions as it is internal and should not be exposed by client libraries. - - Customizing a newly created Span is only supported via passing `SpanOptions` to `createSpanFunction` +- @azure/core-tracing has been rewritten in order to provide cleaner abstractions for client libraries as well as remove @opentelemetry/api as a direct dependency. + - @opentelemetry/api is no longer a direct dependency of @azure/core-tracing providing for smaller bundle sizes and lower incidence of version conflicts + - `createSpanFunction` has been removed and replaced with a stateful `TracingClient` which can be created using the `createTracingClient` function. + - `TracingClient` introduces a new API for creating tracing spans. Use `TracingClient#withSpan` to wrap an invocation in a span, ensuring the span is ended and exceptions are captured. + - `TracingClient` also provides the lower-level APIs necessary to start a span without making it active, create request headers, serialize `traceparent` header, and wrapping a callback with an active context. ### Bugs Fixed From ed382d7ce0f892bf5bcfa10f359fd283d3529b7c Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Wed, 12 Jan 2022 14:31:00 -0800 Subject: [PATCH 6/7] instrumentation changelog --- .../CHANGELOG.md | 10 ++++++++++ .../src/instrumenter.ts | 12 ------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md index 4144f75694a0..3940e21b480c 100644 --- a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md @@ -1,3 +1,13 @@ # Release History ## 1.0.0-beta.1 (Unreleased) + +### Features Added + +This marks the first beta release of the OpenTelemetry Instrumentation library for the Azure SDK which will enable OpenTelemetry Span creation for Azure SDK client libraries. + +### Breaking Changes + +### Bugs Fixed + +### Other Changes diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts index 7df341abdc51..8cd86717849d 100644 --- a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts @@ -49,29 +49,17 @@ export class OpenTelemetryInstrumenter implements Instrumenter { ); } -<<<<<<< HEAD parseTraceparentHeader(traceparentHeader: string): TracingContext { return propagator.extract( context.active(), { traceparent: traceparentHeader }, defaultTextMapGetter ); -======= - parseTraceparentHeader(traceparentHeader: string): TracingSpanContext | undefined { - const newContext = propagation.extract(context.active(), { traceparent: traceparentHeader }); - console.log("parseTraceparentHeader", trace.getSpanContext(newContext), traceparentHeader); - return trace.getSpanContext(newContext); ->>>>>>> 130571e16... wip } createRequestHeaders(tracingContext?: TracingContext): Record { const headers: Record = {}; -<<<<<<< HEAD propagator.inject(tracingContext || context.active(), headers, defaultTextMapSetter); -======= - propagation.inject(tracingContext || context.active(), headers); - console.log("createRequestHeaders", headers); ->>>>>>> 130571e16... wip return headers; } } From 52944fc63014e63b94b14eed9eb195a509f730cb Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Wed, 12 Jan 2022 14:32:51 -0800 Subject: [PATCH 7/7] Revert "wip" This reverts commit 4fa61ed9a0e86905120c828e1cd5b5f61bf476d4. --- sdk/core/core-http/package.json | 2 +- sdk/core/core-http/review/core-http.api.md | 19 +- sdk/core/core-http/src/createSpanLegacy.ts | 12 +- .../core-http/src/policies/tracingPolicy.ts | 74 +- sdk/core/core-http/src/webResource.ts | 23 +- .../test/policies/tracingPolicyTests.ts | 866 +++--- sdk/eventhub/event-hubs/package.json | 2 +- .../event-hubs/review/event-hubs.api.md | 8 +- .../src/diagnostics/instrumentEventData.ts | 26 +- .../event-hubs/src/diagnostics/tracing.ts | 98 +- sdk/eventhub/event-hubs/src/eventDataBatch.ts | 13 +- .../event-hubs/src/eventHubProducerClient.ts | 22 +- .../event-hubs/src/managementClient.ts | 15 +- .../event-hubs/src/partitionProcessor.ts | 11 +- sdk/eventhub/event-hubs/src/partitionPump.ts | 31 +- .../event-hubs/test/internal/misc.spec.ts | 955 +++---- .../test/internal/partitionPump.spec.ts | 330 +-- .../event-hubs/test/internal/sender.spec.ts | 2538 ++++++++--------- .../event-hubs/test/public/hubruntime.spec.ts | 564 ++-- .../keyvault-common/src/tracingHelpers.ts | 55 +- .../test/utils/supportsTracing.ts | 41 +- sdk/keyvault/keyvault-secrets/package.json | 2 +- sdk/keyvault/keyvault-secrets/src/index.ts | 82 +- .../src/lro/delete/operation.ts | 14 +- .../src/lro/recover/operation.ts | 12 +- .../keyvault-secrets/test/public/CRUD.spec.ts | 9 +- 26 files changed, 2958 insertions(+), 2866 deletions(-) diff --git a/sdk/core/core-http/package.json b/sdk/core/core-http/package.json index fafd1a35693a..27791f771830 100644 --- a/sdk/core/core-http/package.json +++ b/sdk/core/core-http/package.json @@ -115,7 +115,7 @@ "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.14", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", diff --git a/sdk/core/core-http/review/core-http.api.md b/sdk/core/core-http/review/core-http.api.md index a5cdf950b168..3c58a4b3c93f 100644 --- a/sdk/core/core-http/review/core-http.api.md +++ b/sdk/core/core-http/review/core-http.api.md @@ -8,13 +8,14 @@ import { AbortSignalLike } from '@azure/abort-controller'; import { AccessToken } from '@azure/core-auth'; +import { Context } from '@azure/core-tracing'; import { Debugger } from '@azure/logger'; import { GetTokenOptions } from '@azure/core-auth'; import { isTokenCredential } from '@azure/core-auth'; import { OperationTracingOptions } from '@azure/core-tracing'; +import { Span } from '@azure/core-tracing'; +import { SpanOptions } from '@azure/core-tracing'; import { TokenCredential } from '@azure/core-auth'; -import { TracingContext } from '@azure/core-tracing'; -import { TracingSpan } from '@azure/core-tracing'; export { AbortSignalLike } @@ -167,8 +168,8 @@ export const Constants: { export function createPipelineFromOptions(pipelineOptions: InternalPipelineOptions, authPolicyFactory?: RequestPolicyFactory): ServiceClientOptions; // @public @deprecated -export function createSpanFunction(_args: SpanConfig): (operationName: string, operationOptions: T) => { - span: TracingSpan; +export function createSpanFunction(args: SpanConfig): (operationName: string, operationOptions: T) => { + span: Span; updatedOptions: T; }; @@ -581,7 +582,7 @@ export interface RequestOptionsBase { serializerOptions?: SerializerOptions; shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean); timeout?: number; - tracingContext?: TracingContext; + tracingContext?: Context; } // @public @@ -636,7 +637,8 @@ export interface RequestPrepareOptions { [key: string]: any | ParameterValue; }; serializationMapper?: Mapper; - tracingContext?: TracingContext; + spanOptions?: SpanOptions; + tracingContext?: Context; url?: string; } @@ -869,11 +871,12 @@ export class WebResource implements WebResourceLike { }; requestId: string; shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean); + spanOptions?: SpanOptions; // @deprecated streamResponseBody?: boolean; streamResponseStatusCodes?: Set; timeout: number; - tracingContext?: TracingContext; + tracingContext?: Context; url: string; validateRequestProperties(): void; withCredentials: boolean; @@ -904,7 +907,7 @@ export interface WebResourceLike { streamResponseBody?: boolean; streamResponseStatusCodes?: Set; timeout: number; - tracingContext?: TracingContext; + tracingContext?: Context; url: string; validateRequestProperties(): void; withCredentials: boolean; diff --git a/sdk/core/core-http/src/createSpanLegacy.ts b/sdk/core/core-http/src/createSpanLegacy.ts index 491aae11dbb1..23eebc647d6f 100644 --- a/sdk/core/core-http/src/createSpanLegacy.ts +++ b/sdk/core/core-http/src/createSpanLegacy.ts @@ -5,7 +5,7 @@ // were a part of the GA'd library and can't be removed until the next major // release. They currently get called always, even if tracing is not enabled. -import { createTracingClient, TracingSpan } from "@azure/core-tracing"; +import { Span, createSpanFunction as coreTracingCreateSpanFunction } from "@azure/core-tracing"; import { OperationOptions } from "./operationOptions"; /** @@ -35,14 +35,10 @@ export interface SpanConfig { * @param tracingOptions - The options for the underlying http request. */ export function createSpanFunction( - _args: SpanConfig + args: SpanConfig ): ( operationName: string, operationOptions: T -) => { span: TracingSpan; updatedOptions: T } { - return createTracingClient({ - namespace: "Microsoft.CoreHttp", - packageName: "@azure/core-http", - packageVersion: "foo", - }).startSpan; +) => { span: Span; updatedOptions: T } { + return coreTracingCreateSpanFunction(args); } diff --git a/sdk/core/core-http/src/policies/tracingPolicy.ts b/sdk/core/core-http/src/policies/tracingPolicy.ts index b9f18fec251c..2d359cc5bc8d 100644 --- a/sdk/core/core-http/src/policies/tracingPolicy.ts +++ b/sdk/core/core-http/src/policies/tracingPolicy.ts @@ -8,16 +8,23 @@ import { RequestPolicyOptions, } from "./requestPolicy"; import { - createTracingClient, - TracingClient, - TracingContext, - TracingSpan, + Span, + SpanKind, + SpanStatusCode, + createSpanFunction, + getTraceParentHeader, + isSpanContextValid, } from "@azure/core-tracing"; import { HttpOperationResponse } from "../httpOperationResponse"; import { URLBuilder } from "../url"; import { WebResourceLike } from "../webResource"; import { logger } from "../log"; +const createSpan = createSpanFunction({ + packagePrefix: "", + namespace: "", +}); + /** * Options to customize the tracing policy. */ @@ -46,7 +53,6 @@ export function tracingPolicy(tracingOptions: TracingPolicyOptions = {}): Reques */ export class TracingPolicy extends BaseRequestPolicy { private userAgent?: string; - private tracingClient: TracingClient; constructor( nextPolicy: RequestPolicy, @@ -55,11 +61,6 @@ export class TracingPolicy extends BaseRequestPolicy { ) { super(nextPolicy, options); this.userAgent = tracingOptions.userAgent; - this.tracingClient = createTracingClient({ - namespace: "Microsoft.CoreHttp", - packageName: "@azure/core-http", - packageVersion: "foo", - }); } public async sendRequest(request: WebResourceLike): Promise { @@ -67,16 +68,14 @@ export class TracingPolicy extends BaseRequestPolicy { return this._nextPolicy.sendRequest(request); } - const { span, context } = this.tryCreateSpan(request); + const span = this.tryCreateSpan(request); if (!span) { return this._nextPolicy.sendRequest(request); } try { - const response = await this.tracingClient.withContext(context!, () => - this._nextPolicy.sendRequest(request) - ); + const response = await this._nextPolicy.sendRequest(request); this.tryProcessResponse(span, response); return response; } catch (err) { @@ -85,20 +84,17 @@ export class TracingPolicy extends BaseRequestPolicy { } } - tryCreateSpan(request: WebResourceLike): { - span: TracingSpan | undefined; - context: TracingContext | undefined; - } { + tryCreateSpan(request: WebResourceLike): Span | undefined { try { const path = URLBuilder.parse(request.url).getPath() || "/"; // Passing spanOptions as part of tracingOptions to maintain compatibility @azure/core-tracing@preview.13 and earlier. // We can pass this as a separate parameter once we upgrade to the latest core-tracing. - const { span, updatedOptions } = this.tracingClient.startSpan(path, { + const { span } = createSpan(path, { tracingOptions: { spanOptions: { ...(request as any).spanOptions, - spanKind: "client", + kind: SpanKind.CLIENT, }, tracingContext: request.tracingContext, }, @@ -107,7 +103,7 @@ export class TracingPolicy extends BaseRequestPolicy { // If the span is not recording, don't do any more work. if (!span.isRecording()) { span.end(); - return { context: updatedOptions.tracingOptions.tracingContext, span: undefined }; + return undefined; } const namespaceFromContext = request.tracingContext?.getValue(Symbol.for("az.namespace")); @@ -116,33 +112,39 @@ export class TracingPolicy extends BaseRequestPolicy { span.setAttribute("az.namespace", namespaceFromContext); } - span.setAttribute("http.method", request.method); - span.setAttribute("http.url", request.url); - span.setAttribute("requestId", request.requestId); + span.setAttributes({ + "http.method": request.method, + "http.url": request.url, + requestId: request.requestId, + }); if (this.userAgent) { span.setAttribute("http.user_agent", this.userAgent); } // set headers - const headers = this.tracingClient.createRequestHeaders( - updatedOptions.tracingOptions?.tracingContext - ); - for (const header in headers) { - request.headers.set(header, headers[header]); + const spanContext = span.spanContext(); + const traceParentHeader = getTraceParentHeader(spanContext); + if (traceParentHeader && isSpanContextValid(spanContext)) { + request.headers.set("traceparent", traceParentHeader); + const traceState = spanContext.traceState && spanContext.traceState.serialize(); + // if tracestate is set, traceparent MUST be set, so only set tracestate after traceparent + if (traceState) { + request.headers.set("tracestate", traceState); + } } - return { span, context: updatedOptions.tracingOptions.tracingContext }; + return span; } catch (error) { logger.warning(`Skipping creating a tracing span due to an error: ${error.message}`); - return { context: undefined, span: undefined }; + return undefined; } } - private tryProcessError(span: TracingSpan, err: any): void { + private tryProcessError(span: Span, err: any): void { try { span.setStatus({ - status: "error", - error: err, + code: SpanStatusCode.ERROR, + message: err.message, }); if (err.statusCode) { @@ -154,7 +156,7 @@ export class TracingPolicy extends BaseRequestPolicy { } } - private tryProcessResponse(span: TracingSpan, response: HttpOperationResponse): void { + private tryProcessResponse(span: Span, response: HttpOperationResponse): void { try { span.setAttribute("http.status_code", response.status); const serviceRequestId = response.headers.get("x-ms-request-id"); @@ -162,7 +164,7 @@ export class TracingPolicy extends BaseRequestPolicy { span.setAttribute("serviceRequestId", serviceRequestId); } span.setStatus({ - status: "success", + code: SpanStatusCode.OK, }); span.end(); } catch (error) { diff --git a/sdk/core/core-http/src/webResource.ts b/sdk/core/core-http/src/webResource.ts index a1816d9a0cd2..cd1378a2ff32 100644 --- a/sdk/core/core-http/src/webResource.ts +++ b/sdk/core/core-http/src/webResource.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { TracingContext } from "@azure/core-tracing"; +import { Context, SpanOptions } from "@azure/core-tracing"; import { HttpHeaders, HttpHeadersLike, isHttpHeadersLike } from "./httpHeaders"; import { Mapper, Serializer } from "./serializer"; import { AbortSignalLike } from "@azure/abort-controller"; @@ -142,7 +142,7 @@ export interface WebResourceLike { /** * Tracing: Context used when creating spans. */ - tracingContext?: TracingContext; + tracingContext?: Context; /** * Validates that the required properties such as method, url, headers["Content-Type"], @@ -284,10 +284,15 @@ export class WebResource implements WebResourceLike { */ onDownloadProgress?: (progress: TransferProgressEvent) => void; + /** + * Tracing: Options used to create a span when tracing is enabled. + */ + spanOptions?: SpanOptions; + /** * Tracing: Context used when creating Spans. */ - tracingContext?: TracingContext; + tracingContext?: Context; constructor( url?: string, @@ -545,6 +550,10 @@ export class WebResource implements WebResourceLike { } } + if (options.spanOptions) { + this.spanOptions = options.spanOptions; + } + if (options.tracingContext) { this.tracingContext = options.tracingContext; } @@ -702,10 +711,14 @@ export interface RequestPrepareOptions { * Allows keeping track of the progress of downloading the incoming response. */ onDownloadProgress?: (progress: TransferProgressEvent) => void; + /** + * Tracing: Options used to create a span when tracing is enabled. + */ + spanOptions?: SpanOptions; /** * Tracing: Context used when creating spans. */ - tracingContext?: TracingContext; + tracingContext?: Context; } /** @@ -765,7 +778,7 @@ export interface RequestOptionsBase { /** * Tracing: Context used when creating spans. */ - tracingContext?: TracingContext; + tracingContext?: Context; /** * May contain other properties. diff --git a/sdk/core/core-http/test/policies/tracingPolicyTests.ts b/sdk/core/core-http/test/policies/tracingPolicyTests.ts index 0417654f7ea8..75a52d512ab2 100644 --- a/sdk/core/core-http/test/policies/tracingPolicyTests.ts +++ b/sdk/core/core-http/test/policies/tracingPolicyTests.ts @@ -1,433 +1,433 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT license. - -// import { -// HttpHeaders, -// HttpOperationResponse, -// RequestPolicy, -// RequestPolicyOptions, -// WebResource, -// } from "../../src/coreHttp"; -// import { Span, SpanOptions, Tracer, TracerProvider, trace } from "@opentelemetry/api"; -// import { -// SpanAttributeValue, -// SpanAttributes, -// SpanContext, -// SpanStatus, -// SpanStatusCode, -// TraceFlags, -// TraceState, -// context, -// setSpan, -// } from "@azure/core-tracing"; -// import { assert } from "chai"; -// import sinon from "sinon"; -// import { tracingPolicy } from "../../src/policies/tracingPolicy"; - -// class MockSpan implements Span { -// private _endCalled = false; -// private _status: SpanStatus = { -// code: SpanStatusCode.UNSET, -// }; -// private _attributes: SpanAttributes = {}; - -// constructor( -// private traceId: string, -// private spanId: string, -// private flags: TraceFlags, -// private state: string, -// options?: SpanOptions -// ) { -// this._attributes = options?.attributes || {}; -// } - -// addEvent(): this { -// throw new Error("Method not implemented."); -// } - -// isRecording(): boolean { -// return true; -// } - -// recordException(): void { -// throw new Error("Method not implemented."); -// } - -// updateName(): this { -// throw new Error("Method not implemented."); -// } - -// didEnd(): boolean { -// return this._endCalled; -// } - -// end(): void { -// this._endCalled = true; -// } - -// getStatus() { -// return this._status; -// } - -// setStatus(status: SpanStatus) { -// this._status = status; -// return this; -// } - -// setAttributes(attributes: SpanAttributes): this { -// for (const key in attributes) { -// this.setAttribute(key, attributes[key]!); -// } -// return this; -// } - -// setAttribute(key: string, value: SpanAttributeValue) { -// this._attributes[key] = value; -// return this; -// } - -// getAttribute(key: string) { -// return this._attributes[key]; -// } - -// spanContext(): SpanContext { -// const state = this.state; - -// const traceState = { -// set(): TraceState { -// /* empty */ -// return traceState; -// }, -// unset(): TraceState { -// /* empty */ -// return traceState; -// }, -// get(): string | undefined { -// return; -// }, -// serialize() { -// return state; -// }, -// }; - -// return { -// traceId: this.traceId, -// spanId: this.spanId, -// traceFlags: this.flags, -// traceState, -// }; -// } -// } - -// class MockTracer implements Tracer { -// private spans: MockSpan[] = []; -// private _startSpanCalled = false; - -// constructor( -// private traceId = "", -// private spanId = "", -// private flags = TraceFlags.NONE, -// private state = "" -// ) {} - -// startActiveSpan(): never { -// throw new Error("Method not implemented."); -// } - -// getStartedSpans(): MockSpan[] { -// return this.spans; -// } - -// startSpanCalled(): boolean { -// return this._startSpanCalled; -// } - -// startSpan(_name: string, options?: SpanOptions): MockSpan { -// this._startSpanCalled = true; -// const span = new MockSpan(this.traceId, this.spanId, this.flags, this.state, options); -// this.spans.push(span); -// return span; -// } -// } - -// class MockTracerProvider implements TracerProvider { -// private mockTracer: Tracer = new MockTracer(); - -// setTracer(tracer: Tracer) { -// this.mockTracer = tracer; -// } - -// getTracer(): Tracer { -// return this.mockTracer; -// } - -// register() { -// trace.setGlobalTracerProvider(this); -// } - -// disable() { -// trace.disable(); -// } -// } - -// const ROOT_SPAN = new MockSpan("root", "root", TraceFlags.SAMPLED, ""); - -// describe("tracingPolicy", function () { -// const TRACE_VERSION = "00"; -// const mockTracerProvider = new MockTracerProvider(); - -// const mockPolicy: RequestPolicy = { -// sendRequest(request: WebResource): Promise { -// return Promise.resolve({ -// request: request, -// status: 200, -// headers: new HttpHeaders(), -// }); -// }, -// }; - -// beforeEach(() => { -// mockTracerProvider.register(); -// }); - -// afterEach(() => { -// mockTracerProvider.disable(); -// }); - -// it("will not create a span if tracingContext is missing", async () => { -// const mockTracer = new MockTracer(); -// const request = new WebResource(); -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isFalse(mockTracer.startSpanCalled()); -// }); - -// it("will create a span and correctly set trace headers if tracingContext is available", async () => { -// const mockTraceId = "11111111111111111111111111111111"; -// const mockSpanId = "2222222222222222"; -// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED); -// mockTracerProvider.setTracer(mockTracer); - -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.isTrue(span.didEnd()); - -// const expectedFlag = "01"; - -// assert.equal( -// request.headers.get("traceparent"), -// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` -// ); -// assert.notExists(request.headers.get("tracestate")); -// }); - -// it("will create a span and correctly set trace headers if tracingContext is available (no TraceOptions)", async () => { -// const mockTraceId = "11111111111111111111111111111111"; -// const mockSpanId = "2222222222222222"; -// // leave out the TraceOptions -// const mockTracer = new MockTracer(mockTraceId, mockSpanId); -// mockTracerProvider.setTracer(mockTracer); - -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.isTrue(span.didEnd()); -// assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); -// assert.equal(span.getAttribute("http.status_code"), 200); - -// const expectedFlag = "00"; - -// assert.equal( -// request.headers.get("traceparent"), -// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` -// ); -// assert.notExists(request.headers.get("tracestate")); -// }); - -// it("will create a span and correctly set trace headers if tracingContext is available (TraceState)", async () => { -// const mockTraceId = "11111111111111111111111111111111"; -// const mockSpanId = "2222222222222222"; -// const mockTraceState = "foo=bar"; -// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); -// mockTracerProvider.setTracer(mockTracer); -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.isTrue(span.didEnd()); -// assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); -// assert.equal(span.getAttribute("http.status_code"), 200); - -// const expectedFlag = "01"; - -// assert.equal( -// request.headers.get("traceparent"), -// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` -// ); -// assert.equal(request.headers.get("tracestate"), mockTraceState); -// }); - -// it("will close a span if an error is encountered", async () => { -// const mockTraceId = "11111111111111111111111111111111"; -// const mockSpanId = "2222222222222222"; -// const mockTraceState = "foo=bar"; -// const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); -// mockTracerProvider.setTracer(mockTracer); -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create( -// { -// sendRequest(requestParam: WebResource): Promise { -// return Promise.reject({ -// request: requestParam, -// statusCode: 400, -// headers: new HttpHeaders(), -// message: "Bad Request.", -// }); -// }, -// }, -// new RequestPolicyOptions() -// ); -// try { -// await policy.sendRequest(request); -// throw new Error("Test Failure"); -// } catch (err) { -// assert.notEqual(err.message, "Test Failure"); -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.isTrue(span.didEnd()); -// assert.deepEqual(span.getStatus(), { -// code: SpanStatusCode.ERROR, -// message: "Bad Request.", -// }); -// assert.equal(span.getAttribute("http.status_code"), 400); - -// const expectedFlag = "01"; - -// assert.equal( -// request.headers.get("traceparent"), -// `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` -// ); -// assert.equal(request.headers.get("tracestate"), mockTraceState); -// } -// }); - -// it("will not set headers if span is a NoOpSpan", async () => { -// mockTracerProvider.disable(); -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.notExists(request.headers.get("traceparent")); -// assert.notExists(request.headers.get("tracestate")); -// }); - -// it("will not set headers if context is invalid", async () => { -// // This will create a tracer that produces invalid trace-id and span-id -// const mockTracer = new MockTracer("invalid", "00", TraceFlags.SAMPLED, "foo=bar"); -// mockTracerProvider.setTracer(mockTracer); - -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.notExists(request.headers.get("traceparent")); -// assert.notExists(request.headers.get("tracestate")); -// }); - -// it("will not fail the request if span setup fails", async () => { -// const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); -// sinon.stub(errorTracer, "startSpan").throws(new Error("Test Error")); -// mockTracerProvider.setTracer(errorTracer); - -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - -// const response = await policy.sendRequest(request); -// assert.equal(response.status, 200); -// }); - -// it("will not fail the request if response processing fails", async () => { -// const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); -// mockTracerProvider.setTracer(errorTracer); -// const errorSpan = new MockSpan("", "", TraceFlags.SAMPLED, ""); -// sinon.stub(errorSpan, "end").throws(new Error("Test Error")); -// sinon.stub(errorTracer, "startSpan").returns(errorSpan); - -// const request = new WebResource(); -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); - -// const response = await policy.sendRequest(request); -// assert.equal(response.status, 200); -// }); - -// it("will give priority to context's az.namespace over spanOptions", async () => { -// const mockTracer = new MockTracer(); -// mockTracerProvider.setTracer(mockTracer); - -// const request = new WebResource(); -// request.spanOptions = { -// attributes: { "az.namespace": "value_from_span_options" }, -// }; -// request.tracingContext = setSpan(context.active(), ROOT_SPAN).setValue( -// Symbol.for("az.namespace"), -// "value_from_context" -// ); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.equal(span.getAttribute("az.namespace"), "value_from_context"); -// }); - -// it("will use spanOptions if context does not have az.namespace", async () => { -// const mockTracer = new MockTracer(); -// mockTracerProvider.setTracer(mockTracer); - -// const request = new WebResource(); -// request.spanOptions = { -// attributes: { "az.namespace": "value_from_span_options" }, -// }; -// request.tracingContext = setSpan(context.active(), ROOT_SPAN); - -// const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); -// await policy.sendRequest(request); - -// assert.isTrue(mockTracer.startSpanCalled()); -// assert.lengthOf(mockTracer.getStartedSpans(), 1); -// const span = mockTracer.getStartedSpans()[0]; -// assert.equal(span.getAttribute("az.namespace"), "value_from_span_options"); -// }); -// }); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + HttpHeaders, + HttpOperationResponse, + RequestPolicy, + RequestPolicyOptions, + WebResource, +} from "../../src/coreHttp"; +import { Span, SpanOptions, Tracer, TracerProvider, trace } from "@opentelemetry/api"; +import { + SpanAttributeValue, + SpanAttributes, + SpanContext, + SpanStatus, + SpanStatusCode, + TraceFlags, + TraceState, + context, + setSpan, +} from "@azure/core-tracing"; +import { assert } from "chai"; +import sinon from "sinon"; +import { tracingPolicy } from "../../src/policies/tracingPolicy"; + +class MockSpan implements Span { + private _endCalled = false; + private _status: SpanStatus = { + code: SpanStatusCode.UNSET, + }; + private _attributes: SpanAttributes = {}; + + constructor( + private traceId: string, + private spanId: string, + private flags: TraceFlags, + private state: string, + options?: SpanOptions + ) { + this._attributes = options?.attributes || {}; + } + + addEvent(): this { + throw new Error("Method not implemented."); + } + + isRecording(): boolean { + return true; + } + + recordException(): void { + throw new Error("Method not implemented."); + } + + updateName(): this { + throw new Error("Method not implemented."); + } + + didEnd(): boolean { + return this._endCalled; + } + + end(): void { + this._endCalled = true; + } + + getStatus() { + return this._status; + } + + setStatus(status: SpanStatus) { + this._status = status; + return this; + } + + setAttributes(attributes: SpanAttributes): this { + for (const key in attributes) { + this.setAttribute(key, attributes[key]!); + } + return this; + } + + setAttribute(key: string, value: SpanAttributeValue) { + this._attributes[key] = value; + return this; + } + + getAttribute(key: string) { + return this._attributes[key]; + } + + spanContext(): SpanContext { + const state = this.state; + + const traceState = { + set(): TraceState { + /* empty */ + return traceState; + }, + unset(): TraceState { + /* empty */ + return traceState; + }, + get(): string | undefined { + return; + }, + serialize() { + return state; + }, + }; + + return { + traceId: this.traceId, + spanId: this.spanId, + traceFlags: this.flags, + traceState, + }; + } +} + +class MockTracer implements Tracer { + private spans: MockSpan[] = []; + private _startSpanCalled = false; + + constructor( + private traceId = "", + private spanId = "", + private flags = TraceFlags.NONE, + private state = "" + ) {} + + startActiveSpan(): never { + throw new Error("Method not implemented."); + } + + getStartedSpans(): MockSpan[] { + return this.spans; + } + + startSpanCalled(): boolean { + return this._startSpanCalled; + } + + startSpan(_name: string, options?: SpanOptions): MockSpan { + this._startSpanCalled = true; + const span = new MockSpan(this.traceId, this.spanId, this.flags, this.state, options); + this.spans.push(span); + return span; + } +} + +class MockTracerProvider implements TracerProvider { + private mockTracer: Tracer = new MockTracer(); + + setTracer(tracer: Tracer) { + this.mockTracer = tracer; + } + + getTracer(): Tracer { + return this.mockTracer; + } + + register() { + trace.setGlobalTracerProvider(this); + } + + disable() { + trace.disable(); + } +} + +const ROOT_SPAN = new MockSpan("root", "root", TraceFlags.SAMPLED, ""); + +describe("tracingPolicy", function () { + const TRACE_VERSION = "00"; + const mockTracerProvider = new MockTracerProvider(); + + const mockPolicy: RequestPolicy = { + sendRequest(request: WebResource): Promise { + return Promise.resolve({ + request: request, + status: 200, + headers: new HttpHeaders(), + }); + }, + }; + + beforeEach(() => { + mockTracerProvider.register(); + }); + + afterEach(() => { + mockTracerProvider.disable(); + }); + + it("will not create a span if tracingContext is missing", async () => { + const mockTracer = new MockTracer(); + const request = new WebResource(); + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isFalse(mockTracer.startSpanCalled()); + }); + + it("will create a span and correctly set trace headers if tracingContext is available", async () => { + const mockTraceId = "11111111111111111111111111111111"; + const mockSpanId = "2222222222222222"; + const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED); + mockTracerProvider.setTracer(mockTracer); + + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.isTrue(span.didEnd()); + + const expectedFlag = "01"; + + assert.equal( + request.headers.get("traceparent"), + `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` + ); + assert.notExists(request.headers.get("tracestate")); + }); + + it("will create a span and correctly set trace headers if tracingContext is available (no TraceOptions)", async () => { + const mockTraceId = "11111111111111111111111111111111"; + const mockSpanId = "2222222222222222"; + // leave out the TraceOptions + const mockTracer = new MockTracer(mockTraceId, mockSpanId); + mockTracerProvider.setTracer(mockTracer); + + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.isTrue(span.didEnd()); + assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); + assert.equal(span.getAttribute("http.status_code"), 200); + + const expectedFlag = "00"; + + assert.equal( + request.headers.get("traceparent"), + `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` + ); + assert.notExists(request.headers.get("tracestate")); + }); + + it("will create a span and correctly set trace headers if tracingContext is available (TraceState)", async () => { + const mockTraceId = "11111111111111111111111111111111"; + const mockSpanId = "2222222222222222"; + const mockTraceState = "foo=bar"; + const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); + mockTracerProvider.setTracer(mockTracer); + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.isTrue(span.didEnd()); + assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK }); + assert.equal(span.getAttribute("http.status_code"), 200); + + const expectedFlag = "01"; + + assert.equal( + request.headers.get("traceparent"), + `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` + ); + assert.equal(request.headers.get("tracestate"), mockTraceState); + }); + + it("will close a span if an error is encountered", async () => { + const mockTraceId = "11111111111111111111111111111111"; + const mockSpanId = "2222222222222222"; + const mockTraceState = "foo=bar"; + const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState); + mockTracerProvider.setTracer(mockTracer); + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create( + { + sendRequest(requestParam: WebResource): Promise { + return Promise.reject({ + request: requestParam, + statusCode: 400, + headers: new HttpHeaders(), + message: "Bad Request.", + }); + }, + }, + new RequestPolicyOptions() + ); + try { + await policy.sendRequest(request); + throw new Error("Test Failure"); + } catch (err) { + assert.notEqual(err.message, "Test Failure"); + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.isTrue(span.didEnd()); + assert.deepEqual(span.getStatus(), { + code: SpanStatusCode.ERROR, + message: "Bad Request.", + }); + assert.equal(span.getAttribute("http.status_code"), 400); + + const expectedFlag = "01"; + + assert.equal( + request.headers.get("traceparent"), + `${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}` + ); + assert.equal(request.headers.get("tracestate"), mockTraceState); + } + }); + + it("will not set headers if span is a NoOpSpan", async () => { + mockTracerProvider.disable(); + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.notExists(request.headers.get("traceparent")); + assert.notExists(request.headers.get("tracestate")); + }); + + it("will not set headers if context is invalid", async () => { + // This will create a tracer that produces invalid trace-id and span-id + const mockTracer = new MockTracer("invalid", "00", TraceFlags.SAMPLED, "foo=bar"); + mockTracerProvider.setTracer(mockTracer); + + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.notExists(request.headers.get("traceparent")); + assert.notExists(request.headers.get("tracestate")); + }); + + it("will not fail the request if span setup fails", async () => { + const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); + sinon.stub(errorTracer, "startSpan").throws(new Error("Test Error")); + mockTracerProvider.setTracer(errorTracer); + + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + + const response = await policy.sendRequest(request); + assert.equal(response.status, 200); + }); + + it("will not fail the request if response processing fails", async () => { + const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, ""); + mockTracerProvider.setTracer(errorTracer); + const errorSpan = new MockSpan("", "", TraceFlags.SAMPLED, ""); + sinon.stub(errorSpan, "end").throws(new Error("Test Error")); + sinon.stub(errorTracer, "startSpan").returns(errorSpan); + + const request = new WebResource(); + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + + const response = await policy.sendRequest(request); + assert.equal(response.status, 200); + }); + + it("will give priority to context's az.namespace over spanOptions", async () => { + const mockTracer = new MockTracer(); + mockTracerProvider.setTracer(mockTracer); + + const request = new WebResource(); + request.spanOptions = { + attributes: { "az.namespace": "value_from_span_options" }, + }; + request.tracingContext = setSpan(context.active(), ROOT_SPAN).setValue( + Symbol.for("az.namespace"), + "value_from_context" + ); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.equal(span.getAttribute("az.namespace"), "value_from_context"); + }); + + it("will use spanOptions if context does not have az.namespace", async () => { + const mockTracer = new MockTracer(); + mockTracerProvider.setTracer(mockTracer); + + const request = new WebResource(); + request.spanOptions = { + attributes: { "az.namespace": "value_from_span_options" }, + }; + request.tracingContext = setSpan(context.active(), ROOT_SPAN); + + const policy = tracingPolicy().create(mockPolicy, new RequestPolicyOptions()); + await policy.sendRequest(request); + + assert.isTrue(mockTracer.startSpanCalled()); + assert.lengthOf(mockTracer.getStartedSpans(), 1); + const span = mockTracer.getStartedSpans()[0]; + assert.equal(span.getAttribute("az.namespace"), "value_from_span_options"); + }); +}); diff --git a/sdk/eventhub/event-hubs/package.json b/sdk/eventhub/event-hubs/package.json index a1cc7a4ea0ca..273455e9cee1 100644 --- a/sdk/eventhub/event-hubs/package.json +++ b/sdk/eventhub/event-hubs/package.json @@ -110,7 +110,7 @@ "@azure/core-amqp": "^3.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.14", + "@azure/core-tracing": "1.0.0-preview.13", "@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 ca017e974512..6d028766d38c 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -15,9 +15,9 @@ 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 { TracingSpan } from '@azure/core-tracing'; -import { TracingSpanContext } from '@azure/core-tracing'; import { WebSocketImpl } from 'rhea-promise'; import { WebSocketOptions } from '@azure/core-amqp'; @@ -86,7 +86,7 @@ export interface EventDataBatch { _generateMessage(): Buffer; readonly maxSizeInBytes: number; // @internal - readonly _messageSpanContexts: TracingSpanContext[]; + readonly _messageSpanContexts: SpanContext[]; // @internal readonly partitionId?: string; // @internal @@ -354,7 +354,7 @@ export { TokenCredential } // @public export interface TryAddOptions { // @deprecated (undocumented) - parentSpan?: TracingSpan | TracingSpanContext; + 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 00f5446c8c99..308685bcf518 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -3,14 +3,14 @@ import { EventData, isAmqpAnnotatedMessage } from "../eventData"; import { - TracingSpanContext, - // extractSpanContextFromTraceParentHeader, - // getTraceParentHeader, - // isSpanContextValid, + extractSpanContextFromTraceParentHeader, + getTraceParentHeader, + isSpanContextValid, } from "@azure/core-tracing"; import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { OperationOptions } from "../util/operationOptions"; -import { createMessageSpan, tracingClient } from "./tracing"; +import { SpanContext } from "@azure/core-tracing"; +import { createMessageSpan } from "./tracing"; /** * @internal @@ -29,7 +29,7 @@ export function instrumentEventData( options: OperationOptions, entityPath: string, host: string -): { event: EventData; spanContext: TracingSpanContext | undefined } { +): { event: EventData; spanContext: SpanContext | undefined } { const props = isAmqpAnnotatedMessage(eventData) ? eventData.applicationProperties : eventData.properties; @@ -41,7 +41,7 @@ export function instrumentEventData( return { event: eventData, spanContext: undefined }; } - const { span: messageSpan, updatedOptions } = createMessageSpan(options, { entityPath, host }); + const { span: messageSpan } = createMessageSpan(options, { entityPath, host }); try { if (!messageSpan.isRecording()) { return { @@ -50,10 +50,8 @@ export function instrumentEventData( }; } - const traceParent = tracingClient.createRequestHeaders( - updatedOptions.tracingOptions?.tracingContext - )["traceparent"]; - if (traceParent) { + const traceParent = getTraceParentHeader(messageSpan.spanContext()); + if (traceParent && isSpanContextValid(messageSpan.spanContext())) { const copiedProps = { ...props }; // create a copy so the original isn't modified @@ -79,13 +77,11 @@ export function instrumentEventData( * @param eventData - An individual `EventData` object. * @internal */ -export function extractSpanContextFromEventData( - eventData: EventData -): TracingSpanContext | undefined { +export function extractSpanContextFromEventData(eventData: EventData): SpanContext | undefined { if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { return; } const diagnosticId = eventData.properties[TRACEPARENT_PROPERTY]; - return tracingClient.parseTraceparentHeader(diagnosticId); + return extractSpanContextFromTraceParentHeader(diagnosticId); } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts b/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts index a8fbbf55e132..0e87bf4140c0 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/tracing.ts @@ -1,13 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { createTracingClient, TracingSpan, TracingSpanOptions } from "@azure/core-tracing"; +import { + Span, + SpanContext, + SpanKind, + SpanOptions, + context, + createSpanFunction, + setSpan, + setSpanContext, +} from "@azure/core-tracing"; import { EventHubConnectionConfig } from "../eventhubConnectionConfig"; import { OperationOptions } from "../util/operationOptions"; +import { TryAddOptions } from "../eventDataBatch"; -export const tracingClient = createTracingClient({ - namespace: "Azure.EventHubs", - packageName: "@azure/event-hubs", +const _createSpan = createSpanFunction({ + namespace: "Microsoft.EventHub", + packagePrefix: "Azure.EventHubs", }); /** @@ -18,9 +28,9 @@ export function createEventHubSpan( operationName: string, operationOptions: OperationOptions | undefined, connectionConfig: Pick, - additionalSpanOptions?: TracingSpanOptions -): { span: TracingSpan; updatedOptions: OperationOptions } { - const { span, updatedOptions } = tracingClient.startSpan(operationName, { + additionalSpanOptions?: SpanOptions +): { span: Span; updatedOptions: OperationOptions } { + const { span, updatedOptions } = _createSpan(operationName, { ...operationOptions, tracingOptions: { ...operationOptions?.tracingOptions, @@ -49,6 +59,78 @@ export function createMessageSpan( eventHubConfig: Pick ): ReturnType { return createEventHubSpan("message", operationOptions, eventHubConfig, { - spanKind: "producer", + 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; + } + + const x = possibleSpan as Span; + return typeof x.spanContext === "function"; +} diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 3e64895983ad..37a80c9c2d83 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -3,10 +3,12 @@ import { EventData, toRheaMessage } from "./eventData"; import { MessageAnnotations, Message as RheaMessage, message } from "rhea-promise"; +import { Span, SpanContext } from "@azure/core-tracing"; import { isDefined, isObjectWithProperties } from "./util/typeGuards"; import { AmqpAnnotatedMessage } from "@azure/core-amqp"; import { ConnectionContext } from "./connectionContext"; -import { OperationTracingOptions, TracingSpan, TracingSpanContext } from "@azure/core-tracing"; +import { OperationTracingOptions } from "@azure/core-tracing"; +import { convertTryAddOptionsForCompatibility } from "./diagnostics/tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; import { throwTypeErrorIfParameterMissing } from "./util/error"; @@ -49,7 +51,7 @@ export interface TryAddOptions { /** * @deprecated Tracing options have been moved to the `tracingOptions` property. */ - parentSpan?: TracingSpan | TracingSpanContext; + parentSpan?: Span | SpanContext; } /** @@ -123,7 +125,7 @@ export interface EventDataBatch { * Used internally by the `sendBatch()` method to set up the right spans in traces if tracing is enabled. * @internal */ - readonly _messageSpanContexts: TracingSpanContext[]; + readonly _messageSpanContexts: SpanContext[]; } /** @@ -166,7 +168,7 @@ export class EventDataBatchImpl implements EventDataBatch { /** * List of 'message' span contexts. */ - private _spanContexts: TracingSpanContext[] = []; + private _spanContexts: SpanContext[] = []; /** * The message annotations to apply on the batch envelope. * This will reflect the message annotations on the first event @@ -241,7 +243,7 @@ export class EventDataBatchImpl implements EventDataBatch { * Gets the "message" span contexts that were created when adding events to the batch. * @internal */ - get _messageSpanContexts(): TracingSpanContext[] { + get _messageSpanContexts(): SpanContext[] { return this._spanContexts; } @@ -284,6 +286,7 @@ 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 e70905f1795a..6d2b68a00101 100644 --- a/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubProducerClient.ts @@ -12,7 +12,7 @@ import { } from "./models/public"; import { EventDataBatch, EventDataBatchImpl, isEventDataBatch } from "./eventDataBatch"; import { EventHubProperties, PartitionProperties } from "./managementClient"; -import { TracingSpan, TracingSpanContext, TracingSpanLink } from "@azure/core-tracing"; +import { Link, Span, SpanContext, SpanKind, SpanStatusCode } from "@azure/core-tracing"; import { NamedKeyCredential, SASCredential, TokenCredential } from "@azure/core-auth"; import { isCredential, isDefined } from "./util/typeGuards"; import { logErrorStackTrace, logger } from "./log"; @@ -284,7 +284,7 @@ export class EventHubProducerClient { let partitionKey: string | undefined; // link message span contexts - let spanContextsToLink: TracingSpanContext[] = []; + let spanContextsToLink: SpanContext[] = []; if (isEventDataBatch(batch)) { // For batches, partitionId and partitionKey would be set on the batch. @@ -350,12 +350,12 @@ export class EventHubProducerClient { partitionKey, retryOptions: this._clientOptions.retryOptions, }); - sendSpan.setStatus({ status: "success" }); + sendSpan.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { sendSpan.setStatus({ - status: "error", - error, + code: SpanStatusCode.ERROR, + message: error.message, }); throw error; } finally { @@ -431,17 +431,17 @@ export class EventHubProducerClient { private _createSendSpan( operationOptions: OperationOptions, - spanContextsToLink: TracingSpanContext[] = [] - ): TracingSpan { - const spanLinks: TracingSpanLink[] = spanContextsToLink.map((context) => { + spanContextsToLink: SpanContext[] = [] + ): Span { + const links: Link[] = spanContextsToLink.map((context) => { return { - spanContext: context, + context, }; }); const { span } = createEventHubSpan("send", operationOptions, this._context.config, { - spanKind: "client", - spanLinks, + 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 995fc3c0f381..3712f4f534e5 100644 --- a/sdk/eventhub/event-hubs/src/managementClient.ts +++ b/sdk/eventhub/event-hubs/src/managementClient.ts @@ -29,7 +29,7 @@ import { AccessToken } from "@azure/core-auth"; import { ConnectionContext } from "./connectionContext"; import { LinkEntity } from "./linkEntity"; import { OperationOptions } from "./util/operationOptions"; -import {} from "@azure/core-tracing"; +import { SpanStatusCode } from "@azure/core-tracing"; import { createEventHubSpan } from "./diagnostics/tracing"; import { getRetryAttemptTimeoutInMs } from "./util/retries"; import { v4 as uuid } from "uuid"; @@ -190,10 +190,13 @@ export class ManagementClient extends LinkEntity { }; logger.verbose("[%s] The hub runtime info is: %O", this._context.connectionId, runtimeInfo); - clientSpan.setStatus({ status: "success" }); + clientSpan.setStatus({ code: SpanStatusCode.OK }); return runtimeInfo; } catch (error) { - clientSpan.setStatus({ status: "error", error }); + clientSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); logger.warning( `An error occurred while getting the hub runtime information: ${error?.name}: ${error?.message}` ); @@ -258,13 +261,13 @@ export class ManagementClient extends LinkEntity { }; logger.verbose("[%s] The partition info is: %O.", this._context.connectionId, partitionInfo); - clientSpan.setStatus({ status: "success" }); + clientSpan.setStatus({ code: SpanStatusCode.OK }); return partitionInfo; } catch (error) { clientSpan.setStatus({ - status: "error", - error, + code: SpanStatusCode.ERROR, + message: error.message, }); logger.warning( `An error occurred while getting the partition information: ${error?.name}: ${error?.message}` diff --git a/sdk/eventhub/event-hubs/src/partitionProcessor.ts b/sdk/eventhub/event-hubs/src/partitionProcessor.ts index 6468e7a368e3..c074a3fc7c4f 100644 --- a/sdk/eventhub/event-hubs/src/partitionProcessor.ts +++ b/sdk/eventhub/event-hubs/src/partitionProcessor.ts @@ -11,8 +11,6 @@ import { CloseReason } from "./models/public"; import { LastEnqueuedEventProperties } from "./eventHubReceiver"; import { ReceivedEventData } from "./eventData"; import { logger } from "./log"; -import { tracingClient } from "./diagnostics/tracing"; -import { OperationOptions } from "./util/operationOptions"; /** * A checkpoint is meant to represent the last successfully processed event by the user from a particular @@ -160,13 +158,8 @@ export class PartitionProcessor implements PartitionContext { * * @param event - The received events to be processed. */ - async processEvents( - events: ReceivedEventData[], - updatedOptions: OperationOptions - ): Promise { - await tracingClient.withContext(updatedOptions.tracingOptions!.tracingContext!, async () => { - await this._eventHandlers.processEvents(events, this); - }); + async processEvents(events: ReceivedEventData[]): Promise { + await this._eventHandlers.processEvents(events, this); } /** diff --git a/sdk/eventhub/event-hubs/src/partitionPump.ts b/sdk/eventhub/event-hubs/src/partitionPump.ts index 594bbc238b50..fe44c0992280 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 { TracingSpan, TracingSpanLink } from "@azure/core-tracing"; +import { Link, Span, SpanKind, SpanStatusCode } from "@azure/core-tracing"; import { logErrorStackTrace, logger } from "./log"; import { AbortController } from "@azure/abort-controller"; import { CloseReason } from "./models/public"; @@ -131,16 +131,13 @@ export class PartitionPump { lastSeenSequenceNumber = receivedEvents[receivedEvents.length - 1].sequenceNumber; } - const { span, updatedOptions } = createProcessingSpan( + const span = createProcessingSpan( receivedEvents, this._context.config, this._processorOptions ); - await trace( - () => this._partitionProcessor.processEvents(receivedEvents, updatedOptions), - span - ); + 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 @@ -215,8 +212,8 @@ export function createProcessingSpan( receivedEvents: ReceivedEventData[], eventHubProperties: Pick, options?: OperationOptions -): { span: TracingSpan; updatedOptions: OperationOptions } { - const links: TracingSpanLink[] = []; +): Span { + const links: Link[] = []; for (const receivedEvent of receivedEvents) { const spanContext = extractSpanContextFromEventData(receivedEvent); @@ -226,32 +223,32 @@ export function createProcessingSpan( } links.push({ - spanContext, + context: spanContext, attributes: { enqueuedTime: receivedEvent.enqueuedTimeUtc.getTime(), }, }); } - const { span, updatedOptions } = createEventHubSpan("process", options, eventHubProperties, { - spanKind: "consumer", - spanLinks: links, + const { span } = createEventHubSpan("process", options, eventHubProperties, { + kind: SpanKind.CONSUMER, + links, }); - return { span, updatedOptions }; + return span; } /** * @internal */ -export async function trace(fn: () => Promise, span: TracingSpan): Promise { +export async function trace(fn: () => Promise, span: Span): Promise { try { await fn(); - span.setStatus({ status: "success" }); + span.setStatus({ code: SpanStatusCode.OK }); } catch (err) { span.setStatus({ - status: "error", - error: err, + code: SpanStatusCode.ERROR, + message: err.message, }); throw err; } finally { diff --git a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts index 5081ee812a83..8fd955161266 100644 --- a/sdk/eventhub/event-hubs/test/internal/misc.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/misc.spec.ts @@ -1,477 +1,478 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT license. - -// import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils"; -// import { -// EventData, -// EventHubConsumerClient, -// EventHubProducerClient, -// EventHubProperties, -// ReceivedEventData, -// Subscription, -// } from "../../src"; -// import { -// TRACEPARENT_PROPERTY, -// extractSpanContextFromEventData, -// } from "../../src/diagnostics/instrumentEventData"; -// import { SubscriptionHandlerForTests } from "../public/utils/subscriptionHandlerForTests"; -// 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"; - -// const should = chai.should(); -// chai.use(chaiAsPromised); -// const debug = debugModule("azure:event-hubs:misc-spec"); - -// testWithServiceTypes((serviceVersion) => { -// const env = getEnvVars(); -// if (serviceVersion === "mock") { -// let service: ReturnType; -// before("Starting mock service", () => { -// service = createMockServer(); -// return service.start(); -// }); - -// after("Stopping mock service", () => { -// return service?.stop(); -// }); -// } - -// describe("Misc tests", function (): void { -// const service = { -// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// path: env[EnvVarKeys.EVENTHUB_NAME], -// }; -// let consumerClient: EventHubConsumerClient; -// let producerClient: EventHubProducerClient; -// let hubInfo: EventHubProperties; -// let partitionId: string; -// let lastEnqueuedOffset: number; - -// before("validate environment", async function (): Promise { -// should.exist( -// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." -// ); -// should.exist( -// env[EnvVarKeys.EVENTHUB_NAME], -// "define EVENTHUB_NAME in your environment before running integration tests." -// ); -// }); - -// beforeEach(async () => { -// debug("Creating the clients.."); -// producerClient = new EventHubProducerClient(service.connectionString, service.path); -// consumerClient = new EventHubConsumerClient( -// EventHubConsumerClient.defaultConsumerGroupName, -// service.connectionString, -// service.path -// ); -// hubInfo = await consumerClient.getEventHubProperties(); -// partitionId = hubInfo.partitionIds[0]; -// lastEnqueuedOffset = (await consumerClient.getPartitionProperties(partitionId)) -// .lastEnqueuedOffset; -// }); - -// afterEach(async () => { -// debug("Closing the clients.."); -// await producerClient.close(); -// await consumerClient.close(); -// }); - -// it("should be able to send and receive a large message correctly", async function (): Promise { -// const bodysize = 220 * 1024; -// const msgString = "A".repeat(220 * 1024); -// const msgBody = Buffer.from(msgString); -// const obj: EventData = { body: msgBody }; -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// debug("Sending one message with %d bytes.", bodysize); -// await producerClient.sendBatch([obj], { partitionId }); -// debug("Successfully sent the large message."); - -// let subscription: Subscription | undefined; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// should.exist(data); -// should.equal(data.length, 1); -// should.equal(data[0].body.toString(), msgString); -// should.not.exist((data[0].properties || {}).message_id); -// resolve(); -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// }); - -// it("should be able to send and receive a JSON object as a message correctly", async function (): Promise { -// const msgBody = { -// id: "123-456-789", -// weight: 10, -// isBlue: true, -// siblings: [ -// { -// id: "098-789-564", -// weight: 20, -// isBlue: false, -// }, -// ], -// }; -// const obj: EventData = { body: msgBody }; -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// debug("Sending one message %O", obj); -// await producerClient.sendBatch([obj], { partitionId }); -// debug("Successfully sent the large message."); - -// let subscription: Subscription | undefined; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// should.exist(data); -// should.equal(data.length, 1); -// assert.deepEqual(data[0].body, msgBody); -// should.not.exist((data[0].properties || {}).message_id); -// resolve(); -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// }); - -// it("should be able to send and receive an array as a message correctly", async function (): Promise { -// const msgBody = [ -// { -// id: "098-789-564", -// weight: 20, -// isBlue: false, -// }, -// 10, -// 20, -// "some string", -// ]; -// const obj: EventData = { body: msgBody, properties: { message_id: uuid() } }; -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// debug("Sending one message %O", obj); -// await producerClient.sendBatch([obj], { partitionId }); -// debug("Successfully sent the large message."); - -// let subscription: Subscription | undefined; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// should.exist(data); -// should.equal(data.length, 1); -// assert.deepEqual(data[0].body, msgBody); -// assert.strictEqual(data[0].properties!.message_id, obj.properties!.message_id); -// resolve(); -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// }); - -// it("should be able to send a boolean as a message correctly", async function (): Promise { -// const msgBody = true; -// const obj: EventData = { body: msgBody }; -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// debug("Sending one message %O", obj); -// await producerClient.sendBatch([obj], { partitionId }); -// debug("Successfully sent the large message."); - -// let subscription: Subscription | undefined; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// should.exist(data); -// should.equal(data.length, 1); -// assert.deepEqual(data[0].body, msgBody); -// should.not.exist((data[0].properties || {}).message_id); -// resolve(); -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// }); - -// it("should be able to send and receive batched messages correctly ", async function (): Promise { -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// const messageCount = 5; -// const d: EventData[] = []; -// for (let i = 0; i < messageCount; i++) { -// const obj: EventData = { body: `Hello EH ${i}` }; -// d.push(obj); -// } - -// await producerClient.sendBatch(d, { partitionId }); -// debug("Successfully sent 5 messages batched together."); - -// let subscription: Subscription | undefined; -// const receivedMsgs: ReceivedEventData[] = []; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// receivedMsgs.push(...data); -// if (receivedMsgs.length === 5) { -// resolve(); -// } -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// receivedMsgs.length.should.equal(5); -// for (const message of receivedMsgs) { -// should.not.exist((message.properties || {}).message_id); -// } -// }); - -// it("should be able to send and receive batched messages as JSON objects correctly ", async function (): Promise { -// debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); -// const messageCount = 5; -// const d: EventData[] = []; -// for (let i = 0; i < messageCount; i++) { -// const obj: EventData = { -// body: { -// id: "123-456-789", -// count: i, -// weight: 10, -// isBlue: true, -// siblings: [ -// { -// id: "098-789-564", -// weight: 20, -// isBlue: false, -// }, -// ], -// }, -// properties: { -// message_id: uuid(), -// }, -// }; -// d.push(obj); -// } - -// await producerClient.sendBatch(d, { partitionId }); -// debug("Successfully sent 5 messages batched together."); - -// let subscription: Subscription | undefined; -// const receivedMsgs: ReceivedEventData[] = []; -// await new Promise((resolve, reject) => { -// subscription = consumerClient.subscribe( -// partitionId, -// { -// processEvents: async (data) => { -// debug("received message: ", data.length); -// receivedMsgs.push(...data); -// if (receivedMsgs.length === 5) { -// resolve(); -// } -// }, -// processError: async (err) => { -// reject(err); -// }, -// }, -// { -// startPosition: { offset: lastEnqueuedOffset }, -// } -// ); -// }); -// await subscription!.close(); -// should.equal(receivedMsgs[0].body.count, 0); -// should.equal(receivedMsgs.length, 5); -// for (const [index, message] of receivedMsgs.entries()) { -// assert.strictEqual(message.properties!.message_id, d[index].properties!.message_id); -// } -// }); - -// it("should consistently send messages with partitionkey to a partitionId", async function (): Promise { -// const { subscriptionEventHandler, startPosition } = -// await SubscriptionHandlerForTests.startingFromHere(consumerClient); - -// const msgToSendCount = 50; -// debug("Sending %d messages.", msgToSendCount); - -// function getRandomInt(max: number): number { -// return Math.floor(Math.random() * Math.floor(max)); -// } - -// const senderPromises = []; - -// for (let i = 0; i < msgToSendCount; i++) { -// const partitionKey = getRandomInt(10); -// senderPromises.push( -// producerClient.sendBatch([{ body: "Hello EventHub " + i }], { -// partitionKey: partitionKey.toString(), -// }) -// ); -// } - -// await Promise.all(senderPromises); - -// debug("Starting to receive all messages from each partition."); -// const partitionMap: any = {}; - -// let subscription: Subscription | undefined = undefined; - -// try { -// subscription = consumerClient.subscribe(subscriptionEventHandler, { -// startPosition, -// }); -// const receivedEvents = await subscriptionEventHandler.waitForFullEvents( -// hubInfo.partitionIds, -// msgToSendCount -// ); - -// for (const d of receivedEvents) { -// debug(">>>> _raw_amqp_mesage: ", (d as any)._raw_amqp_mesage); -// const pk = d.event.partitionKey as string; -// debug("pk: ", pk); - -// if (partitionMap[pk] && partitionMap[pk] !== d.partitionId) { -// debug( -// `#### Error: Received a message from partition ${d.partitionId} with partition key ${pk}, whereas the same key was observed on partition ${partitionMap[pk]} before.` -// ); -// assert(partitionMap[pk] === d.partitionId); -// } -// partitionMap[pk] = d.partitionId; -// debug("partitionMap ", partitionMap); -// } -// } finally { -// if (subscription) { -// await subscription.close(); -// } -// await consumerClient.close(); -// } -// }); -// }).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"; -// const eventData: ReceivedEventData = { -// body: "This is a test.", -// enqueuedTimeUtc: new Date(), -// offset: 0, -// sequenceNumber: 0, -// partitionKey: null, -// properties: { -// [TRACEPARENT_PROPERTY]: `00-${traceId}-${spanId}-${flags}`, -// }, -// 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." -// ); -// }); - -// it("should return undefined when EventData is not instrumented", function () { -// const eventData: ReceivedEventData = { -// body: "This is a test.", -// enqueuedTimeUtc: new Date(), -// offset: 0, -// sequenceNumber: 0, -// partitionKey: null, -// getRawAmqpMessage() { -// return {} as any; -// }, -// }; - -// const spanContext = extractSpanContextFromEventData(eventData); - -// should.not.exist( -// spanContext, -// `Missing property "${TRACEPARENT_PROPERTY}" should return undefined spanContext.` -// ); -// }); -// }); -// }); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils"; +import { + EventData, + EventHubConsumerClient, + EventHubProducerClient, + EventHubProperties, + ReceivedEventData, + Subscription, +} from "../../src"; +import { + TRACEPARENT_PROPERTY, + 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"; + +const should = chai.should(); +chai.use(chaiAsPromised); +const debug = debugModule("azure:event-hubs:misc-spec"); + +testWithServiceTypes((serviceVersion) => { + const env = getEnvVars(); + if (serviceVersion === "mock") { + let service: ReturnType; + before("Starting mock service", () => { + service = createMockServer(); + return service.start(); + }); + + after("Stopping mock service", () => { + return service?.stop(); + }); + } + + describe("Misc tests", function (): void { + const service = { + connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + path: env[EnvVarKeys.EVENTHUB_NAME], + }; + let consumerClient: EventHubConsumerClient; + let producerClient: EventHubProducerClient; + let hubInfo: EventHubProperties; + let partitionId: string; + let lastEnqueuedOffset: number; + + before("validate environment", async function (): Promise { + should.exist( + env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." + ); + should.exist( + env[EnvVarKeys.EVENTHUB_NAME], + "define EVENTHUB_NAME in your environment before running integration tests." + ); + }); + + beforeEach(async () => { + debug("Creating the clients.."); + producerClient = new EventHubProducerClient(service.connectionString, service.path); + consumerClient = new EventHubConsumerClient( + EventHubConsumerClient.defaultConsumerGroupName, + service.connectionString, + service.path + ); + hubInfo = await consumerClient.getEventHubProperties(); + partitionId = hubInfo.partitionIds[0]; + lastEnqueuedOffset = (await consumerClient.getPartitionProperties(partitionId)) + .lastEnqueuedOffset; + }); + + afterEach(async () => { + debug("Closing the clients.."); + await producerClient.close(); + await consumerClient.close(); + }); + + it("should be able to send and receive a large message correctly", async function (): Promise { + const bodysize = 220 * 1024; + const msgString = "A".repeat(220 * 1024); + const msgBody = Buffer.from(msgString); + const obj: EventData = { body: msgBody }; + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + debug("Sending one message with %d bytes.", bodysize); + await producerClient.sendBatch([obj], { partitionId }); + debug("Successfully sent the large message."); + + let subscription: Subscription | undefined; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + should.exist(data); + should.equal(data.length, 1); + should.equal(data[0].body.toString(), msgString); + should.not.exist((data[0].properties || {}).message_id); + resolve(); + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + }); + + it("should be able to send and receive a JSON object as a message correctly", async function (): Promise { + const msgBody = { + id: "123-456-789", + weight: 10, + isBlue: true, + siblings: [ + { + id: "098-789-564", + weight: 20, + isBlue: false, + }, + ], + }; + const obj: EventData = { body: msgBody }; + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + debug("Sending one message %O", obj); + await producerClient.sendBatch([obj], { partitionId }); + debug("Successfully sent the large message."); + + let subscription: Subscription | undefined; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + should.exist(data); + should.equal(data.length, 1); + assert.deepEqual(data[0].body, msgBody); + should.not.exist((data[0].properties || {}).message_id); + resolve(); + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + }); + + it("should be able to send and receive an array as a message correctly", async function (): Promise { + const msgBody = [ + { + id: "098-789-564", + weight: 20, + isBlue: false, + }, + 10, + 20, + "some string", + ]; + const obj: EventData = { body: msgBody, properties: { message_id: uuid() } }; + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + debug("Sending one message %O", obj); + await producerClient.sendBatch([obj], { partitionId }); + debug("Successfully sent the large message."); + + let subscription: Subscription | undefined; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + should.exist(data); + should.equal(data.length, 1); + assert.deepEqual(data[0].body, msgBody); + assert.strictEqual(data[0].properties!.message_id, obj.properties!.message_id); + resolve(); + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + }); + + it("should be able to send a boolean as a message correctly", async function (): Promise { + const msgBody = true; + const obj: EventData = { body: msgBody }; + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + debug("Sending one message %O", obj); + await producerClient.sendBatch([obj], { partitionId }); + debug("Successfully sent the large message."); + + let subscription: Subscription | undefined; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + should.exist(data); + should.equal(data.length, 1); + assert.deepEqual(data[0].body, msgBody); + should.not.exist((data[0].properties || {}).message_id); + resolve(); + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + }); + + it("should be able to send and receive batched messages correctly ", async function (): Promise { + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + const messageCount = 5; + const d: EventData[] = []; + for (let i = 0; i < messageCount; i++) { + const obj: EventData = { body: `Hello EH ${i}` }; + d.push(obj); + } + + await producerClient.sendBatch(d, { partitionId }); + debug("Successfully sent 5 messages batched together."); + + let subscription: Subscription | undefined; + const receivedMsgs: ReceivedEventData[] = []; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + receivedMsgs.push(...data); + if (receivedMsgs.length === 5) { + resolve(); + } + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + receivedMsgs.length.should.equal(5); + for (const message of receivedMsgs) { + should.not.exist((message.properties || {}).message_id); + } + }); + + it("should be able to send and receive batched messages as JSON objects correctly ", async function (): Promise { + debug(`Partition ${partitionId} has last message with offset ${lastEnqueuedOffset}.`); + const messageCount = 5; + const d: EventData[] = []; + for (let i = 0; i < messageCount; i++) { + const obj: EventData = { + body: { + id: "123-456-789", + count: i, + weight: 10, + isBlue: true, + siblings: [ + { + id: "098-789-564", + weight: 20, + isBlue: false, + }, + ], + }, + properties: { + message_id: uuid(), + }, + }; + d.push(obj); + } + + await producerClient.sendBatch(d, { partitionId }); + debug("Successfully sent 5 messages batched together."); + + let subscription: Subscription | undefined; + const receivedMsgs: ReceivedEventData[] = []; + await new Promise((resolve, reject) => { + subscription = consumerClient.subscribe( + partitionId, + { + processEvents: async (data) => { + debug("received message: ", data.length); + receivedMsgs.push(...data); + if (receivedMsgs.length === 5) { + resolve(); + } + }, + processError: async (err) => { + reject(err); + }, + }, + { + startPosition: { offset: lastEnqueuedOffset }, + } + ); + }); + await subscription!.close(); + should.equal(receivedMsgs[0].body.count, 0); + should.equal(receivedMsgs.length, 5); + for (const [index, message] of receivedMsgs.entries()) { + assert.strictEqual(message.properties!.message_id, d[index].properties!.message_id); + } + }); + + it("should consistently send messages with partitionkey to a partitionId", async function (): Promise { + const { subscriptionEventHandler, startPosition } = + await SubscriptionHandlerForTests.startingFromHere(consumerClient); + + const msgToSendCount = 50; + debug("Sending %d messages.", msgToSendCount); + + function getRandomInt(max: number): number { + return Math.floor(Math.random() * Math.floor(max)); + } + + const senderPromises = []; + + for (let i = 0; i < msgToSendCount; i++) { + const partitionKey = getRandomInt(10); + senderPromises.push( + producerClient.sendBatch([{ body: "Hello EventHub " + i }], { + partitionKey: partitionKey.toString(), + }) + ); + } + + await Promise.all(senderPromises); + + debug("Starting to receive all messages from each partition."); + const partitionMap: any = {}; + + let subscription: Subscription | undefined = undefined; + + try { + subscription = consumerClient.subscribe(subscriptionEventHandler, { + startPosition, + }); + const receivedEvents = await subscriptionEventHandler.waitForFullEvents( + hubInfo.partitionIds, + msgToSendCount + ); + + for (const d of receivedEvents) { + debug(">>>> _raw_amqp_mesage: ", (d as any)._raw_amqp_mesage); + const pk = d.event.partitionKey as string; + debug("pk: ", pk); + + if (partitionMap[pk] && partitionMap[pk] !== d.partitionId) { + debug( + `#### Error: Received a message from partition ${d.partitionId} with partition key ${pk}, whereas the same key was observed on partition ${partitionMap[pk]} before.` + ); + assert(partitionMap[pk] === d.partitionId); + } + partitionMap[pk] = d.partitionId; + debug("partitionMap ", partitionMap); + } + } finally { + if (subscription) { + await subscription.close(); + } + await consumerClient.close(); + } + }); + }).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"; + const eventData: ReceivedEventData = { + body: "This is a test.", + enqueuedTimeUtc: new Date(), + offset: 0, + sequenceNumber: 0, + partitionKey: null, + properties: { + [TRACEPARENT_PROPERTY]: `00-${traceId}-${spanId}-${flags}`, + }, + 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." + ); + }); + + it("should return undefined when EventData is not instrumented", function () { + const eventData: ReceivedEventData = { + body: "This is a test.", + enqueuedTimeUtc: new Date(), + offset: 0, + sequenceNumber: 0, + partitionKey: null, + getRawAmqpMessage() { + return {} as any; + }, + }; + + const spanContext = extractSpanContextFromEventData(eventData); + + should.not.exist( + spanContext, + `Missing property "${TRACEPARENT_PROPERTY}" should return undefined spanContext.` + ); + }); + }); +}); diff --git a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts index 4b5880d87792..413d198094c5 100644 --- a/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/partitionPump.spec.ts @@ -1,165 +1,165 @@ -// // 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 { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; - -// const should = chai.should(); - -// testWithServiceTypes(() => { -// describe("PartitionPump", () => { -// 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, -// }); - -// 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()), -// }, -// }, -// "entityPath", -// "host" -// ).event as ReceivedEventData, -// { properties: {}, ...requiredEventProperties }, // no diagnostic ID means it gets skipped -// instrumentEventData( -// { ...requiredEventProperties }, -// { -// tracingOptions: { -// tracingContext: setSpanContext(context.active(), thirdEvent.spanContext()), -// }, -// }, -// "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); -// }); -// }); -// }); -// }); +// 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 { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; + +const should = chai.should(); + +testWithServiceTypes(() => { + describe("PartitionPump", () => { + 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, + }); + + 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()), + }, + }, + "entityPath", + "host" + ).event as ReceivedEventData, + { properties: {}, ...requiredEventProperties }, // no diagnostic ID means it gets skipped + instrumentEventData( + { ...requiredEventProperties }, + { + tracingOptions: { + tracingContext: setSpanContext(context.active(), thirdEvent.spanContext()), + }, + }, + "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); + }); + }); + }); +}); diff --git a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts index 3c8f3acaf0f2..2df75bcb862b 100644 --- a/sdk/eventhub/event-hubs/test/internal/sender.spec.ts +++ b/sdk/eventhub/event-hubs/test/internal/sender.spec.ts @@ -1,1269 +1,1269 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT license. - -// import { -// EnvVarKeys, -// getEnvVars, -// getStartingPositionsForTests, -// setTracerForTest, -// } 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 { 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"; -// import debugModule from "debug"; -// import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; - -// const should = chai.should(); -// chai.use(chaiAsPromised); -// const debug = debugModule("azure:event-hubs:sender-spec"); - -// testWithServiceTypes((serviceVersion) => { -// const env = getEnvVars(); -// if (serviceVersion === "mock") { -// let service: ReturnType; -// before("Starting mock service", () => { -// service = createMockServer(); -// return service.start(); -// }); - -// after("Stopping mock service", () => { -// return service?.stop(); -// }); -// } - -// describe("EventHub Sender", function (): void { -// const service = { -// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// path: env[EnvVarKeys.EVENTHUB_NAME], -// }; -// let producerClient: EventHubProducerClient; -// let consumerClient: EventHubConsumerClient; -// let startPosition: { [partitionId: string]: EventPosition }; - -// before("validate environment", function (): void { -// should.exist( -// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." -// ); -// should.exist( -// env[EnvVarKeys.EVENTHUB_NAME], -// "define EVENTHUB_NAME in your environment before running integration tests." -// ); -// }); - -// beforeEach(async () => { -// debug("Creating the clients.."); -// producerClient = new EventHubProducerClient(service.connectionString, service.path); -// consumerClient = new EventHubConsumerClient( -// EventHubConsumerClient.defaultConsumerGroupName, -// service.connectionString, -// service.path -// ); -// startPosition = await getStartingPositionsForTests(consumerClient); -// }); - -// afterEach(async () => { -// debug("Closing the clients.."); -// await producerClient.close(); -// await consumerClient.close(); -// }); - -// describe("Create batch", function (): void { -// describe("tryAdd", function () { -// it("doesn't grow if invalid events are added", async () => { -// const batch = await producerClient.createBatch({ maxSizeInBytes: 20 }); -// const event = { body: Buffer.alloc(30).toString() }; - -// const numToAdd = 5; -// let failures = 0; -// for (let i = 0; i < numToAdd; i++) { -// if (!batch.tryAdd(event)) { -// failures++; -// } -// } - -// failures.should.equal(5); -// batch.sizeInBytes.should.equal(0); -// }); -// }); - -// it("partitionId is set as expected", async () => { -// const batch = await producerClient.createBatch({ -// partitionId: "0", -// }); -// should.equal(batch.partitionId, "0"); -// }); - -// it("partitionId is set as expected when it is 0 i.e. falsy", async () => { -// const batch = await producerClient.createBatch({ -// // @ts-expect-error Testing the value 0 is not ignored. -// partitionId: 0, -// }); -// should.equal(batch.partitionId, "0"); -// }); - -// it("partitionKey is set as expected", async () => { -// const batch = await producerClient.createBatch({ -// partitionKey: "boo", -// }); -// should.equal(batch.partitionKey, "boo"); -// }); - -// it("partitionKey is set as expected when it is 0 i.e. falsy", async () => { -// const batch = await producerClient.createBatch({ -// // @ts-expect-error Testing the value 0 is not ignored. -// partitionKey: 0, -// }); -// should.equal(batch.partitionKey, "0"); -// }); - -// it("maxSizeInBytes is set as expected", async () => { -// const batch = await producerClient.createBatch({ maxSizeInBytes: 30 }); -// should.equal(batch.maxSizeInBytes, 30); -// }); - -// it("should be sent successfully", async function (): Promise { -// const list = ["Albert", `${Buffer.from("Mike".repeat(1300000))}`, "Marie"]; - -// const batch = await producerClient.createBatch({ -// partitionId: "0", -// }); - -// batch.partitionId!.should.equal("0"); -// should.not.exist(batch.partitionKey); -// batch.maxSizeInBytes.should.be.gt(0); - -// should.equal(batch.tryAdd({ body: list[0] }), true); -// should.equal(batch.tryAdd({ body: list[1] }), false); // The Mike message will be rejected - it's over the limit. -// should.equal(batch.tryAdd({ body: list[2] }), true); // Marie should get added"; - -// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( -// producerClient -// ); - -// const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { -// startPosition, -// }); -// await producerClient.sendBatch(batch); - -// let receivedEvents; - -// try { -// receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); -// } finally { -// await subscriber.close(); -// } - -// // Mike didn't make it - the message was too big for the batch -// // and was rejected above. -// [list[0], list[2]].should.be.deep.eq( -// receivedEvents.map((event) => event.body), -// "Received messages should be equal to our sent messages" -// ); -// }); - -// it("should be sent successfully when partitionId is 0 i.e. falsy", async function (): Promise { -// const list = ["Albert", "Marie"]; - -// const batch = await producerClient.createBatch({ -// // @ts-expect-error Testing the value 0 is not ignored. -// partitionId: 0, -// }); - -// batch.partitionId!.should.equal("0"); -// should.not.exist(batch.partitionKey); -// batch.maxSizeInBytes.should.be.gt(0); - -// should.equal(batch.tryAdd({ body: list[0] }), true); -// should.equal(batch.tryAdd({ body: list[1] }), true); - -// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( -// producerClient -// ); - -// const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { -// startPosition, -// }); -// await producerClient.sendBatch(batch); - -// let receivedEvents; - -// try { -// receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); -// } finally { -// await subscriber.close(); -// } - -// list.should.be.deep.eq( -// receivedEvents.map((event) => event.body), -// "Received messages should be equal to our sent messages" -// ); -// }); - -// it("should be sent successfully when partitionKey is 0 i.e. falsy", async function (): Promise { -// const list = ["Albert", "Marie"]; - -// const batch = await producerClient.createBatch({ -// // @ts-expect-error Testing the value 0 is not ignored. -// partitionKey: 0, -// }); - -// batch.partitionKey!.should.equal("0"); -// should.not.exist(batch.partitionId); -// batch.maxSizeInBytes.should.be.gt(0); - -// should.equal(batch.tryAdd({ body: list[0] }), true); -// should.equal(batch.tryAdd({ body: list[1] }), true); - -// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( -// producerClient -// ); - -// const subscriber = consumerClient.subscribe(subscriptionEventHandler, { -// startPosition, -// }); -// await producerClient.sendBatch(batch); - -// let receivedEvents; -// const allPartitionIds = await producerClient.getPartitionIds(); -// try { -// receivedEvents = await subscriptionEventHandler.waitForEvents(allPartitionIds, 2); -// } finally { -// await subscriber.close(); -// } - -// list.should.be.deep.eq( -// receivedEvents.map((event) => event.body), -// "Received messages should be equal to our sent messages" -// ); -// }); - -// it("should be sent successfully with properties", async function (): Promise { -// const properties = { test: "super" }; -// const list = [ -// { body: "Albert-With-Properties", properties }, -// { body: "Mike-With-Properties", properties }, -// { body: "Marie-With-Properties", properties }, -// ]; - -// const batch = await producerClient.createBatch({ -// partitionId: "0", -// }); - -// batch.maxSizeInBytes.should.be.gt(0); - -// should.equal(batch.tryAdd(list[0]), true); -// should.equal(batch.tryAdd(list[1]), true); -// should.equal(batch.tryAdd(list[2]), true); - -// const receivedEvents: ReceivedEventData[] = []; -// let waitUntilEventsReceivedResolver: (value?: any) => void; -// const waitUntilEventsReceived = new Promise( -// (resolve) => (waitUntilEventsReceivedResolver = resolve) -// ); - -// const sequenceNumber = (await consumerClient.getPartitionProperties("0")) -// .lastEnqueuedSequenceNumber; - -// const subscriber = consumerClient.subscribe( -// "0", -// { -// async processError() { -// /* no-op */ -// }, -// async processEvents(events) { -// receivedEvents.push(...events); -// if (receivedEvents.length >= 3) { -// waitUntilEventsReceivedResolver(); -// } -// }, -// }, -// { -// startPosition: { -// sequenceNumber, -// }, -// maxBatchSize: 3, -// } -// ); - -// await producerClient.sendBatch(batch); -// await waitUntilEventsReceived; -// await subscriber.close(); - -// sequenceNumber.should.be.lessThan(receivedEvents[0].sequenceNumber); -// sequenceNumber.should.be.lessThan(receivedEvents[1].sequenceNumber); -// sequenceNumber.should.be.lessThan(receivedEvents[2].sequenceNumber); - -// [list[0], list[1], list[2]].should.be.deep.eq( -// receivedEvents.map((event) => { -// return { -// body: event.body, -// properties: event.properties, -// }; -// }), -// "Received messages should be equal to our sent messages" -// ); -// }); - -// 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", -// }); - -// for (let i = 0; i < 2; i++) { -// eventDataBatch.tryAdd( -// { body: `${list[i].name}` }, -// { -// tracingOptions: { -// tracingContext: setSpan(context.active(), 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: [], -// }, -// { -// 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("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) { -// eventDataBatch.tryAdd(event); -// } - -// should.equal(eventDataBatch.count, 2, "Unexpected number of events in batch."); -// should.equal( -// eventDataBatch["_messageSpanContexts"].length, -// 0, -// "Unexpected number of span contexts in batch." -// ); -// }); - -// function legacyOptionsUsingSpanContext( -// rootSpan: TestSpan -// ): Pick { -// return { -// parentSpan: rootSpan.spanContext(), -// }; -// } - -// function legacyOptionsUsingSpan(rootSpan: TestSpan): Pick { -// return { -// parentSpan: rootSpan, -// }; -// } - -// function modernOptions(rootSpan: TestSpan): OperationOptions { -// return { -// tracingOptions: { -// tracingContext: setSpan(context.active(), rootSpan), -// }, -// }; -// } - -// [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" }]; - -// 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("with partition key should be sent successfully.", async function (): Promise { -// const eventDataBatch = await producerClient.createBatch({ partitionKey: "1" }); -// for (let i = 0; i < 5; i++) { -// eventDataBatch.tryAdd({ body: `Hello World ${i}` }); -// } -// await producerClient.sendBatch(eventDataBatch); -// }); - -// it("with max message size should be sent successfully.", async function (): Promise { -// const eventDataBatch = await producerClient.createBatch({ -// maxSizeInBytes: 5000, -// partitionId: "0", -// }); -// const message = { body: `${Buffer.from("Z".repeat(4096))}` }; -// for (let i = 1; i <= 3; i++) { -// const isAdded = eventDataBatch.tryAdd(message); -// if (!isAdded) { -// debug(`Unable to add ${i} event to the batch`); -// break; -// } -// } -// await producerClient.sendBatch(eventDataBatch); -// eventDataBatch.count.should.equal(1); -// }); -// }); - -// describe("Multiple sendBatch calls", function (): void { -// it("should be sent successfully in parallel", async function (): Promise { -// const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( -// consumerClient -// ); - -// const promises = []; -// for (let i = 0; i < 5; i++) { -// promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); -// } -// await Promise.all(promises); - -// const subscription = await consumerClient.subscribe(subscriptionEventHandler, { -// startPosition, -// }); - -// try { -// const events = await subscriptionEventHandler.waitForEvents( -// await consumerClient.getPartitionIds({}), -// 5 -// ); - -// // we've allowed the server to choose which partition the messages are distributed to -// // so our expectation here is just that all the bodies have arrived -// const bodiesOnly = events.map((evt) => evt.body); -// bodiesOnly.sort(); - -// bodiesOnly.should.deep.equal([ -// "Hello World 0", -// "Hello World 1", -// "Hello World 2", -// "Hello World 3", -// "Hello World 4", -// ]); -// } finally { -// subscription.close(); -// } -// }); - -// it("should be sent successfully in parallel, even when exceeding max event listener count of 1000", async function (): Promise { -// const senderCount = 1200; -// try { -// const promises = []; -// for (let i = 0; i < senderCount; i++) { -// promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); -// } -// await Promise.all(promises); -// } catch (err) { -// debug("An error occurred while running the test: ", err); -// throw err; -// } -// }); - -// it("should be sent successfully in parallel by multiple clients", async function (): Promise { -// const senderCount = 3; -// try { -// const promises = []; -// for (let i = 0; i < senderCount; i++) { -// if (i === 0) { -// debug(">>>>> Sending a message to partition %d", i); -// promises.push( -// await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "0" }) -// ); -// } else if (i === 1) { -// debug(">>>>> Sending a message to partition %d", i); -// promises.push( -// await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "1" }) -// ); -// } else { -// debug(">>>>> Sending a message to the hub when i == %d", i); -// promises.push(await producerClient.sendBatch([{ body: `Hello World ${i}` }])); -// } -// } -// await Promise.all(promises); -// } catch (err) { -// debug("An error occurred while running the test: ", err); -// throw err; -// } -// }); - -// it("should fail when a message greater than 1 MB is sent and succeed when a normal message is sent after that on the same link.", async function (): Promise { -// const data: EventData = { -// body: Buffer.from("Z".repeat(1300000)), -// }; -// try { -// debug("Sending a message of 300KB..."); -// await producerClient.sendBatch([data], { partitionId: "0" }); -// 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 -// ); -// } -// await producerClient.sendBatch([{ body: "Hello World EventHub!!" }], { partitionId: "0" }); -// 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"); - -// 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: [ -// { -// 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: [], -// }, -// ], -// }, -// ], -// }; - -// 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); -// } -// }); - -// it("can be manually traced", async function (): Promise { -// const { tracer, resetTracer } = setTracerForTest(); - -// const rootSpan = tracer.startSpan("root"); - -// 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), -// }, -// }); -// 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.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."); - -// 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(); -// }); - -// 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, { -// 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(); -// }); - -// 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." -// ); -// } -// }); -// }); - -// 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( -// "partitionId and partitionKey cannot both be set when creating a batch" -// ); -// } -// }); - -// 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( -// "partitionId and partitionKey cannot both be set when creating a batch" -// ); -// } -// }); - -// 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( -// "partitionId and partitionKey cannot both be set when creating a batch" -// ); -// } -// }); - -// 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 -// ); -// } -// }); -// }); -// 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"); -// } -// }); -// }); - -// 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("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 -// ); -// } -// }); -// }); -// }); -// }); -// }).timeout(20000); -// }); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + EnvVarKeys, + getEnvVars, + getStartingPositionsForTests, + setTracerForTest, +} 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 { 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"; +import debugModule from "debug"; +import { testWithServiceTypes } from "../public/utils/testWithServiceTypes"; + +const should = chai.should(); +chai.use(chaiAsPromised); +const debug = debugModule("azure:event-hubs:sender-spec"); + +testWithServiceTypes((serviceVersion) => { + const env = getEnvVars(); + if (serviceVersion === "mock") { + let service: ReturnType; + before("Starting mock service", () => { + service = createMockServer(); + return service.start(); + }); + + after("Stopping mock service", () => { + return service?.stop(); + }); + } + + describe("EventHub Sender", function (): void { + const service = { + connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + path: env[EnvVarKeys.EVENTHUB_NAME], + }; + let producerClient: EventHubProducerClient; + let consumerClient: EventHubConsumerClient; + let startPosition: { [partitionId: string]: EventPosition }; + + before("validate environment", function (): void { + should.exist( + env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." + ); + should.exist( + env[EnvVarKeys.EVENTHUB_NAME], + "define EVENTHUB_NAME in your environment before running integration tests." + ); + }); + + beforeEach(async () => { + debug("Creating the clients.."); + producerClient = new EventHubProducerClient(service.connectionString, service.path); + consumerClient = new EventHubConsumerClient( + EventHubConsumerClient.defaultConsumerGroupName, + service.connectionString, + service.path + ); + startPosition = await getStartingPositionsForTests(consumerClient); + }); + + afterEach(async () => { + debug("Closing the clients.."); + await producerClient.close(); + await consumerClient.close(); + }); + + describe("Create batch", function (): void { + describe("tryAdd", function () { + it("doesn't grow if invalid events are added", async () => { + const batch = await producerClient.createBatch({ maxSizeInBytes: 20 }); + const event = { body: Buffer.alloc(30).toString() }; + + const numToAdd = 5; + let failures = 0; + for (let i = 0; i < numToAdd; i++) { + if (!batch.tryAdd(event)) { + failures++; + } + } + + failures.should.equal(5); + batch.sizeInBytes.should.equal(0); + }); + }); + + it("partitionId is set as expected", async () => { + const batch = await producerClient.createBatch({ + partitionId: "0", + }); + should.equal(batch.partitionId, "0"); + }); + + it("partitionId is set as expected when it is 0 i.e. falsy", async () => { + const batch = await producerClient.createBatch({ + // @ts-expect-error Testing the value 0 is not ignored. + partitionId: 0, + }); + should.equal(batch.partitionId, "0"); + }); + + it("partitionKey is set as expected", async () => { + const batch = await producerClient.createBatch({ + partitionKey: "boo", + }); + should.equal(batch.partitionKey, "boo"); + }); + + it("partitionKey is set as expected when it is 0 i.e. falsy", async () => { + const batch = await producerClient.createBatch({ + // @ts-expect-error Testing the value 0 is not ignored. + partitionKey: 0, + }); + should.equal(batch.partitionKey, "0"); + }); + + it("maxSizeInBytes is set as expected", async () => { + const batch = await producerClient.createBatch({ maxSizeInBytes: 30 }); + should.equal(batch.maxSizeInBytes, 30); + }); + + it("should be sent successfully", async function (): Promise { + const list = ["Albert", `${Buffer.from("Mike".repeat(1300000))}`, "Marie"]; + + const batch = await producerClient.createBatch({ + partitionId: "0", + }); + + batch.partitionId!.should.equal("0"); + should.not.exist(batch.partitionKey); + batch.maxSizeInBytes.should.be.gt(0); + + should.equal(batch.tryAdd({ body: list[0] }), true); + should.equal(batch.tryAdd({ body: list[1] }), false); // The Mike message will be rejected - it's over the limit. + should.equal(batch.tryAdd({ body: list[2] }), true); // Marie should get added"; + + const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( + producerClient + ); + + const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { + startPosition, + }); + await producerClient.sendBatch(batch); + + let receivedEvents; + + try { + receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); + } finally { + await subscriber.close(); + } + + // Mike didn't make it - the message was too big for the batch + // and was rejected above. + [list[0], list[2]].should.be.deep.eq( + receivedEvents.map((event) => event.body), + "Received messages should be equal to our sent messages" + ); + }); + + it("should be sent successfully when partitionId is 0 i.e. falsy", async function (): Promise { + const list = ["Albert", "Marie"]; + + const batch = await producerClient.createBatch({ + // @ts-expect-error Testing the value 0 is not ignored. + partitionId: 0, + }); + + batch.partitionId!.should.equal("0"); + should.not.exist(batch.partitionKey); + batch.maxSizeInBytes.should.be.gt(0); + + should.equal(batch.tryAdd({ body: list[0] }), true); + should.equal(batch.tryAdd({ body: list[1] }), true); + + const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( + producerClient + ); + + const subscriber = consumerClient.subscribe("0", subscriptionEventHandler, { + startPosition, + }); + await producerClient.sendBatch(batch); + + let receivedEvents; + + try { + receivedEvents = await subscriptionEventHandler.waitForEvents(["0"], 2); + } finally { + await subscriber.close(); + } + + list.should.be.deep.eq( + receivedEvents.map((event) => event.body), + "Received messages should be equal to our sent messages" + ); + }); + + it("should be sent successfully when partitionKey is 0 i.e. falsy", async function (): Promise { + const list = ["Albert", "Marie"]; + + const batch = await producerClient.createBatch({ + // @ts-expect-error Testing the value 0 is not ignored. + partitionKey: 0, + }); + + batch.partitionKey!.should.equal("0"); + should.not.exist(batch.partitionId); + batch.maxSizeInBytes.should.be.gt(0); + + should.equal(batch.tryAdd({ body: list[0] }), true); + should.equal(batch.tryAdd({ body: list[1] }), true); + + const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( + producerClient + ); + + const subscriber = consumerClient.subscribe(subscriptionEventHandler, { + startPosition, + }); + await producerClient.sendBatch(batch); + + let receivedEvents; + const allPartitionIds = await producerClient.getPartitionIds(); + try { + receivedEvents = await subscriptionEventHandler.waitForEvents(allPartitionIds, 2); + } finally { + await subscriber.close(); + } + + list.should.be.deep.eq( + receivedEvents.map((event) => event.body), + "Received messages should be equal to our sent messages" + ); + }); + + it("should be sent successfully with properties", async function (): Promise { + const properties = { test: "super" }; + const list = [ + { body: "Albert-With-Properties", properties }, + { body: "Mike-With-Properties", properties }, + { body: "Marie-With-Properties", properties }, + ]; + + const batch = await producerClient.createBatch({ + partitionId: "0", + }); + + batch.maxSizeInBytes.should.be.gt(0); + + should.equal(batch.tryAdd(list[0]), true); + should.equal(batch.tryAdd(list[1]), true); + should.equal(batch.tryAdd(list[2]), true); + + const receivedEvents: ReceivedEventData[] = []; + let waitUntilEventsReceivedResolver: (value?: any) => void; + const waitUntilEventsReceived = new Promise( + (resolve) => (waitUntilEventsReceivedResolver = resolve) + ); + + const sequenceNumber = (await consumerClient.getPartitionProperties("0")) + .lastEnqueuedSequenceNumber; + + const subscriber = consumerClient.subscribe( + "0", + { + async processError() { + /* no-op */ + }, + async processEvents(events) { + receivedEvents.push(...events); + if (receivedEvents.length >= 3) { + waitUntilEventsReceivedResolver(); + } + }, + }, + { + startPosition: { + sequenceNumber, + }, + maxBatchSize: 3, + } + ); + + await producerClient.sendBatch(batch); + await waitUntilEventsReceived; + await subscriber.close(); + + sequenceNumber.should.be.lessThan(receivedEvents[0].sequenceNumber); + sequenceNumber.should.be.lessThan(receivedEvents[1].sequenceNumber); + sequenceNumber.should.be.lessThan(receivedEvents[2].sequenceNumber); + + [list[0], list[1], list[2]].should.be.deep.eq( + receivedEvents.map((event) => { + return { + body: event.body, + properties: event.properties, + }; + }), + "Received messages should be equal to our sent messages" + ); + }); + + 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", + }); + + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd( + { body: `${list[i].name}` }, + { + tracingOptions: { + tracingContext: setSpan(context.active(), 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: [], + }, + { + 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("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) { + eventDataBatch.tryAdd(event); + } + + should.equal(eventDataBatch.count, 2, "Unexpected number of events in batch."); + should.equal( + eventDataBatch["_messageSpanContexts"].length, + 0, + "Unexpected number of span contexts in batch." + ); + }); + + function legacyOptionsUsingSpanContext( + rootSpan: TestSpan + ): Pick { + return { + parentSpan: rootSpan.spanContext(), + }; + } + + function legacyOptionsUsingSpan(rootSpan: TestSpan): Pick { + return { + parentSpan: rootSpan, + }; + } + + function modernOptions(rootSpan: TestSpan): OperationOptions { + return { + tracingOptions: { + tracingContext: setSpan(context.active(), rootSpan), + }, + }; + } + + [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" }]; + + 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("with partition key should be sent successfully.", async function (): Promise { + const eventDataBatch = await producerClient.createBatch({ partitionKey: "1" }); + for (let i = 0; i < 5; i++) { + eventDataBatch.tryAdd({ body: `Hello World ${i}` }); + } + await producerClient.sendBatch(eventDataBatch); + }); + + it("with max message size should be sent successfully.", async function (): Promise { + const eventDataBatch = await producerClient.createBatch({ + maxSizeInBytes: 5000, + partitionId: "0", + }); + const message = { body: `${Buffer.from("Z".repeat(4096))}` }; + for (let i = 1; i <= 3; i++) { + const isAdded = eventDataBatch.tryAdd(message); + if (!isAdded) { + debug(`Unable to add ${i} event to the batch`); + break; + } + } + await producerClient.sendBatch(eventDataBatch); + eventDataBatch.count.should.equal(1); + }); + }); + + describe("Multiple sendBatch calls", function (): void { + it("should be sent successfully in parallel", async function (): Promise { + const { subscriptionEventHandler } = await SubscriptionHandlerForTests.startingFromHere( + consumerClient + ); + + const promises = []; + for (let i = 0; i < 5; i++) { + promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); + } + await Promise.all(promises); + + const subscription = await consumerClient.subscribe(subscriptionEventHandler, { + startPosition, + }); + + try { + const events = await subscriptionEventHandler.waitForEvents( + await consumerClient.getPartitionIds({}), + 5 + ); + + // we've allowed the server to choose which partition the messages are distributed to + // so our expectation here is just that all the bodies have arrived + const bodiesOnly = events.map((evt) => evt.body); + bodiesOnly.sort(); + + bodiesOnly.should.deep.equal([ + "Hello World 0", + "Hello World 1", + "Hello World 2", + "Hello World 3", + "Hello World 4", + ]); + } finally { + subscription.close(); + } + }); + + it("should be sent successfully in parallel, even when exceeding max event listener count of 1000", async function (): Promise { + const senderCount = 1200; + try { + const promises = []; + for (let i = 0; i < senderCount; i++) { + promises.push(producerClient.sendBatch([{ body: `Hello World ${i}` }])); + } + await Promise.all(promises); + } catch (err) { + debug("An error occurred while running the test: ", err); + throw err; + } + }); + + it("should be sent successfully in parallel by multiple clients", async function (): Promise { + const senderCount = 3; + try { + const promises = []; + for (let i = 0; i < senderCount; i++) { + if (i === 0) { + debug(">>>>> Sending a message to partition %d", i); + promises.push( + await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "0" }) + ); + } else if (i === 1) { + debug(">>>>> Sending a message to partition %d", i); + promises.push( + await producerClient.sendBatch([{ body: `Hello World ${i}` }], { partitionId: "1" }) + ); + } else { + debug(">>>>> Sending a message to the hub when i == %d", i); + promises.push(await producerClient.sendBatch([{ body: `Hello World ${i}` }])); + } + } + await Promise.all(promises); + } catch (err) { + debug("An error occurred while running the test: ", err); + throw err; + } + }); + + it("should fail when a message greater than 1 MB is sent and succeed when a normal message is sent after that on the same link.", async function (): Promise { + const data: EventData = { + body: Buffer.from("Z".repeat(1300000)), + }; + try { + debug("Sending a message of 300KB..."); + await producerClient.sendBatch([data], { partitionId: "0" }); + 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 + ); + } + await producerClient.sendBatch([{ body: "Hello World EventHub!!" }], { partitionId: "0" }); + 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"); + + 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: [ + { + 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: [], + }, + ], + }, + ], + }; + + 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); + } + }); + + it("can be manually traced", async function (): Promise { + const { tracer, resetTracer } = setTracerForTest(); + + const rootSpan = tracer.startSpan("root"); + + 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), + }, + }); + 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.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."); + + 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(); + }); + + 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, { + 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(); + }); + + 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." + ); + } + }); + }); + + 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( + "partitionId and partitionKey cannot both be set when creating a batch" + ); + } + }); + + 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( + "partitionId and partitionKey cannot both be set when creating a batch" + ); + } + }); + + 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( + "partitionId and partitionKey cannot both be set when creating a batch" + ); + } + }); + + 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 + ); + } + }); + }); + 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"); + } + }); + }); + + 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("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 + ); + } + }); + }); + }); + }); + }).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 7abe624f9625..a80145d9b27b 100644 --- a/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts +++ b/sdk/eventhub/event-hubs/test/public/hubruntime.spec.ts @@ -1,282 +1,282 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT license. - -// import { EnvVarKeys, getEnvVars, setTracerForTest } from "./utils/testUtils"; -// import { -// EventHubBufferedProducerClient, -// EventHubConsumerClient, -// EventHubProducerClient, -// MessagingError, -// } from "../../src"; -// import { context, setSpan } from "@azure/core-tracing"; -// import { SpanGraph } from "@azure/test-utils"; -// import chai from "chai"; -// import chaiAsPromised from "chai-as-promised"; -// import { createMockServer } from "./utils/mockService"; -// import debugModule from "debug"; -// import { testWithServiceTypes } from "./utils/testWithServiceTypes"; - -// const should = chai.should(); -// chai.use(chaiAsPromised); -// const debug = debugModule("azure:event-hubs:hubruntime-spec"); - -// type ClientCommonMethods = Pick< -// EventHubProducerClient, -// "close" | "getEventHubProperties" | "getPartitionIds" | "getPartitionProperties" -// >; - -// testWithServiceTypes((serviceVersion) => { -// const env = getEnvVars(); -// if (serviceVersion === "mock") { -// let service: ReturnType; -// before("Starting mock service", () => { -// service = createMockServer(); -// return service.start(); -// }); - -// after("Stopping mock service", () => { -// return service?.stop(); -// }); -// } - -// describe("RuntimeInformation", function (): void { -// const clientTypes = [ -// "EventHubBufferedProducerClient", -// "EventHubConsumerClient", -// "EventHubProducerClient", -// ] as const; -// const clientMap = new Map(); - -// const service = { -// connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// path: env[EnvVarKeys.EVENTHUB_NAME], -// }; -// before("validate environment", function (): void { -// should.exist( -// env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], -// "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." -// ); -// should.exist( -// env[EnvVarKeys.EVENTHUB_NAME], -// "define EVENTHUB_NAME in your environment before running integration tests." -// ); -// }); - -// beforeEach(async () => { -// debug("Creating the clients.."); -// clientMap.set( -// "EventHubBufferedProducerClient", -// new EventHubBufferedProducerClient(service.connectionString, service.path) -// ); -// clientMap.set( -// "EventHubConsumerClient", -// new EventHubConsumerClient( -// EventHubConsumerClient.defaultConsumerGroupName, -// service.connectionString, -// service.path -// ) -// ); -// clientMap.set( -// "EventHubProducerClient", -// new EventHubProducerClient(service.connectionString, service.path) -// ); -// }); - -// afterEach("close the connection", async function (): Promise { -// for (const client of clientMap.values()) { -// await client?.close(); -// } -// }); - -// function arrayOfIncreasingNumbersFromZero(length: any): Array { -// const result = new Array(length); -// for (let i = 0; i < length; i++) { -// result[i] = `${i}`; -// } -// return result; -// } - -// clientTypes.forEach((clientType) => { -// describe(`${clientType}.getPartitionIds`, () => { -// it("returns an array of partition ids", async () => { -// const client = clientMap.get(clientType)!; -// const ids = await client.getPartitionIds({}); -// ids.should.have.members(arrayOfIncreasingNumbersFromZero(ids.length)); -// }); - -// 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(); -// }); -// }); - -// describe(`${clientType}.getEventHubProperties`, () => { -// it("gets the Event Hub runtime information", async () => { -// const client = clientMap.get(clientType)!; -// const hubRuntimeInfo = await client.getEventHubProperties(); -// hubRuntimeInfo.name.should.equal(service.path); - -// hubRuntimeInfo.partitionIds.should.have.members( -// arrayOfIncreasingNumbersFromZero(hubRuntimeInfo.partitionIds.length) -// ); -// hubRuntimeInfo.createdOn.should.be.instanceof(Date); -// }); - -// 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) -// ); -// 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(); -// }); -// }); - -// describe(`${clientType}.getPartitionProperties`, () => { -// it("should throw an error if partitionId is missing", async () => { -// try { -// const client = clientMap.get(clientType)!; -// await client.getPartitionProperties(undefined as any); -// throw new Error("Test failure"); -// } catch (err) { -// (err as any).name.should.equal("TypeError"); -// (err as any).message.should.equal( -// `getPartitionProperties called without required argument "partitionId"` -// ); -// } -// }); - -// it("gets the partition runtime information with partitionId as a string", async () => { -// const client = clientMap.get(clientType)!; -// const partitionRuntimeInfo = await client.getPartitionProperties("0"); -// 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); -// }); - -// it("gets the partition runtime information with partitionId as a number", async () => { -// const client = clientMap.get(clientType)!; -// const partitionRuntimeInfo = await client.getPartitionProperties(0 as any); -// 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); -// }); - -// it("bubbles up error from service for invalid partitionId", async () => { -// try { -// const client = clientMap.get(clientType)!; -// await client.getPartitionProperties("boo"); -// throw new Error("Test failure"); -// } catch (err) { -// should.exist(err); -// should.equal((err as MessagingError).code, "ArgumentOutOfRangeError"); -// } -// }); - -// 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(); -// }); -// }); -// }); -// }).timeout(60000); -// }); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EnvVarKeys, getEnvVars, setTracerForTest } from "./utils/testUtils"; +import { + EventHubBufferedProducerClient, + EventHubConsumerClient, + EventHubProducerClient, + MessagingError, +} from "../../src"; +import { context, setSpan } from "@azure/core-tracing"; +import { SpanGraph } from "@azure/test-utils"; +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { createMockServer } from "./utils/mockService"; +import debugModule from "debug"; +import { testWithServiceTypes } from "./utils/testWithServiceTypes"; + +const should = chai.should(); +chai.use(chaiAsPromised); +const debug = debugModule("azure:event-hubs:hubruntime-spec"); + +type ClientCommonMethods = Pick< + EventHubProducerClient, + "close" | "getEventHubProperties" | "getPartitionIds" | "getPartitionProperties" +>; + +testWithServiceTypes((serviceVersion) => { + const env = getEnvVars(); + if (serviceVersion === "mock") { + let service: ReturnType; + before("Starting mock service", () => { + service = createMockServer(); + return service.start(); + }); + + after("Stopping mock service", () => { + return service?.stop(); + }); + } + + describe("RuntimeInformation", function (): void { + const clientTypes = [ + "EventHubBufferedProducerClient", + "EventHubConsumerClient", + "EventHubProducerClient", + ] as const; + const clientMap = new Map(); + + const service = { + connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + path: env[EnvVarKeys.EVENTHUB_NAME], + }; + before("validate environment", function (): void { + should.exist( + env[EnvVarKeys.EVENTHUB_CONNECTION_STRING], + "define EVENTHUB_CONNECTION_STRING in your environment before running integration tests." + ); + should.exist( + env[EnvVarKeys.EVENTHUB_NAME], + "define EVENTHUB_NAME in your environment before running integration tests." + ); + }); + + beforeEach(async () => { + debug("Creating the clients.."); + clientMap.set( + "EventHubBufferedProducerClient", + new EventHubBufferedProducerClient(service.connectionString, service.path) + ); + clientMap.set( + "EventHubConsumerClient", + new EventHubConsumerClient( + EventHubConsumerClient.defaultConsumerGroupName, + service.connectionString, + service.path + ) + ); + clientMap.set( + "EventHubProducerClient", + new EventHubProducerClient(service.connectionString, service.path) + ); + }); + + afterEach("close the connection", async function (): Promise { + for (const client of clientMap.values()) { + await client?.close(); + } + }); + + function arrayOfIncreasingNumbersFromZero(length: any): Array { + const result = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = `${i}`; + } + return result; + } + + clientTypes.forEach((clientType) => { + describe(`${clientType}.getPartitionIds`, () => { + it("returns an array of partition ids", async () => { + const client = clientMap.get(clientType)!; + const ids = await client.getPartitionIds({}); + ids.should.have.members(arrayOfIncreasingNumbersFromZero(ids.length)); + }); + + 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(); + }); + }); + + describe(`${clientType}.getEventHubProperties`, () => { + it("gets the Event Hub runtime information", async () => { + const client = clientMap.get(clientType)!; + const hubRuntimeInfo = await client.getEventHubProperties(); + hubRuntimeInfo.name.should.equal(service.path); + + hubRuntimeInfo.partitionIds.should.have.members( + arrayOfIncreasingNumbersFromZero(hubRuntimeInfo.partitionIds.length) + ); + hubRuntimeInfo.createdOn.should.be.instanceof(Date); + }); + + 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) + ); + 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(); + }); + }); + + describe(`${clientType}.getPartitionProperties`, () => { + it("should throw an error if partitionId is missing", async () => { + try { + const client = clientMap.get(clientType)!; + await client.getPartitionProperties(undefined as any); + throw new Error("Test failure"); + } catch (err) { + (err as any).name.should.equal("TypeError"); + (err as any).message.should.equal( + `getPartitionProperties called without required argument "partitionId"` + ); + } + }); + + it("gets the partition runtime information with partitionId as a string", async () => { + const client = clientMap.get(clientType)!; + const partitionRuntimeInfo = await client.getPartitionProperties("0"); + 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); + }); + + it("gets the partition runtime information with partitionId as a number", async () => { + const client = clientMap.get(clientType)!; + const partitionRuntimeInfo = await client.getPartitionProperties(0 as any); + 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); + }); + + it("bubbles up error from service for invalid partitionId", async () => { + try { + const client = clientMap.get(clientType)!; + await client.getPartitionProperties("boo"); + throw new Error("Test failure"); + } catch (err) { + should.exist(err); + should.equal((err as MessagingError).code, "ArgumentOutOfRangeError"); + } + }); + + 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(); + }); + }); + }); + }).timeout(60000); +}); diff --git a/sdk/keyvault/keyvault-common/src/tracingHelpers.ts b/sdk/keyvault/keyvault-common/src/tracingHelpers.ts index 0dc3a6415817..60404941aaf2 100644 --- a/sdk/keyvault/keyvault-common/src/tracingHelpers.ts +++ b/sdk/keyvault/keyvault-common/src/tracingHelpers.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// import { Span, SpanStatusCode, createSpanFunction } from "@azure/core-tracing"; +import { Span, SpanStatusCode, createSpanFunction } from "@azure/core-tracing"; import { OperationOptions } from "@azure/core-http"; /** @@ -19,7 +19,7 @@ export interface TracedFunction { ( operationName: string, options: TOptions, - cb: (options: TOptions, span: any) => Promise + cb: (options: TOptions, span: Span) => Promise ): Promise; } @@ -32,34 +32,33 @@ export interface TracedFunction { * * @internal */ -export function createTraceFunction(_prefix: string): TracedFunction { - // const createSpan = createSpanFunction({ - // namespace: "Microsoft.KeyVault", - // packagePrefix: prefix, - // }); +export function createTraceFunction(prefix: string): TracedFunction { + const createSpan = createSpanFunction({ + namespace: "Microsoft.KeyVault", + packagePrefix: prefix, + }); - return async function (..._args: any[]) { - throw new Error("why are we using this still?"); - // const { updatedOptions, span } = createSpan(operationName, options); + return async function (operationName, options, cb) { + const { updatedOptions, span } = createSpan(operationName, options); - // try { - // // NOTE: we really do need to await on this function here so we can handle any exceptions thrown and properly - // // close the span. - // const result = await cb(updatedOptions, span); + try { + // NOTE: we really do need to await on this function here so we can handle any exceptions thrown and properly + // close the span. + const result = await cb(updatedOptions, span); - // // otel 0.16+ needs this or else the code ends up being set as UNSET - // span.setStatus({ - // code: SpanStatusCode.OK, - // }); - // return result; - // } catch (err) { - // span.setStatus({ - // code: SpanStatusCode.ERROR, - // message: err.message, - // }); - // throw err; - // } finally { - // span.end(); - // } + // otel 0.16+ needs this or else the code ends up being set as UNSET + span.setStatus({ + code: SpanStatusCode.OK, + }); + return result; + } catch (err) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + throw err; + } finally { + span.end(); + } }; } diff --git a/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts b/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts index f7d1a492fc59..a4110b3af896 100644 --- a/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts +++ b/sdk/keyvault/keyvault-common/test/utils/supportsTracing.ts @@ -1,7 +1,38 @@ -// import { setSpan, context as otContext, OperationTracingOptions } from "@azure/core-tracing"; -// import { setTracer } from "@azure/test-utils"; -// import { assert } from "chai"; +import { setSpan, context as otContext, OperationTracingOptions } from "@azure/core-tracing"; +import { setTracer } from "@azure/test-utils"; +import { assert } from "chai"; -// const prefix = "Azure.KeyVault"; +const prefix = "Azure.KeyVault"; -export async function supportsTracing(..._args: any[]): Promise {} +export async function supportsTracing( + callback: (tracingOptions: OperationTracingOptions) => Promise, + children: string[] +): Promise { + const tracer = setTracer(); + const rootSpan = tracer.startSpan("root"); + const tracingContext = setSpan(otContext.active(), rootSpan); + + try { + await callback({ tracingContext }); + } finally { + rootSpan.end(); + } + + // Ensure any spans created by KeyVault are parented correctly + let rootSpans = tracer + .getRootSpans() + .filter((span) => span.name.startsWith(prefix) || span.name === "root"); + + assert.equal(rootSpans.length, 1, "Should only have one root span."); + assert.strictEqual(rootSpan, rootSpans[0], "The root span should match what was passed in."); + + // Ensure top-level children are created correctly. + // Testing the entire tree structure can be tricky as other packages might create their own spans. + const spanGraph = tracer.getSpanGraph(rootSpan.spanContext().traceId); + const directChildren = spanGraph.roots[0].children.map((child) => child.name); + // LROs might poll N times, so we'll make a unique array and compare that. + assert.sameMembers(Array.from(new Set(directChildren)), children); + + // Ensure all spans are properly closed + assert.equal(tracer.getActiveSpans().length, 0, "All spans should have had end called"); +} diff --git a/sdk/keyvault/keyvault-secrets/package.json b/sdk/keyvault/keyvault-secrets/package.json index 1aa815ca843a..d156b52d3cb5 100644 --- a/sdk/keyvault/keyvault-secrets/package.json +++ b/sdk/keyvault/keyvault-secrets/package.json @@ -105,7 +105,7 @@ "@azure/core-http": "^2.0.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.14", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" }, diff --git a/sdk/keyvault/keyvault-secrets/src/index.ts b/sdk/keyvault/keyvault-secrets/src/index.ts index 34fa3a186449..4ac15e5414a7 100644 --- a/sdk/keyvault/keyvault-secrets/src/index.ts +++ b/sdk/keyvault/keyvault-secrets/src/index.ts @@ -52,7 +52,7 @@ import { } from "./secretsModels"; import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "./identifier"; import { getSecretFromSecretBundle } from "./transformations"; -import { createTracingClient, TracingClient } from "@azure/core-tracing"; +import { createTraceFunction } from "../../keyvault-common/src"; export { SecretClientOptions, @@ -84,6 +84,8 @@ export { logger, }; +const withTrace = createTraceFunction("Azure.KeyVault.Secrets.SecretClient"); + /** * The SecretClient provides methods to manage {@link KeyVaultSecret} in * the Azure Key Vault. The client supports creating, retrieving, updating, @@ -102,8 +104,6 @@ export class SecretClient { */ private readonly client: KeyVaultClient; - private readonly tracingClient: TracingClient; - /** * Creates an instance of SecretClient. * @@ -156,12 +156,6 @@ export class SecretClient { }, }; - this.tracingClient = createTracingClient({ - namespace: "Microsoft.KeyVault", - packageName: "@azure/keyvault-secrets", - packageVersion: SDK_VERSION, - }); - this.client = new KeyVaultClient( pipelineOptions.serviceVersion || LATEST_API_VERSION, createPipelineFromOptions(internalPipelineOptions, authPolicy) @@ -183,7 +177,7 @@ export class SecretClient { * @param value - The value of the secret. * @param options - The optional parameters. */ - public async setSecret( + public setSecret( secretName: string, value: string, options: SetSecretOptions = {} @@ -201,7 +195,7 @@ export class SecretClient { }, }; } - return this.tracingClient.withSpan("setSecret", unflattenedOptions, async (updatedOptions) => { + return withTrace("setSecret", unflattenedOptions, async (updatedOptions) => { const response = await this.client.setSecret( this.vaultUrl, secretName, @@ -290,19 +284,15 @@ export class SecretClient { }; } - return this.tracingClient.withSpan( - "updateSecretProperties", - unflattenedOptions, - async (updatedOptions) => { - const response = await this.client.updateSecret( - this.vaultUrl, - secretName, - secretVersion, - updatedOptions - ); - return getSecretFromSecretBundle(response).properties; - } - ); + return withTrace("updateSecretProperties", unflattenedOptions, async (updatedOptions) => { + const response = await this.client.updateSecret( + this.vaultUrl, + secretName, + secretVersion, + updatedOptions + ); + return getSecretFromSecretBundle(response).properties; + }); } /** @@ -318,11 +308,8 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public async getSecret( - secretName: string, - options: GetSecretOptions = {} - ): Promise { - return this.tracingClient.withSpan("getSecret", options, async (updatedOptions) => { + public getSecret(secretName: string, options: GetSecretOptions = {}): Promise { + return withTrace("getSecret", options, async (updatedOptions) => { const response = await this.client.getSecret( this.vaultUrl, secretName, @@ -346,11 +333,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public async getDeletedSecret( + public getDeletedSecret( secretName: string, options: GetDeletedSecretOptions = {} ): Promise { - return this.tracingClient.withSpan("getDeletedSecret", options, async (updatedOptions) => { + return withTrace("getDeletedSecret", options, async (updatedOptions) => { const response = await this.client.getDeletedSecret( this.vaultUrl, secretName, @@ -376,11 +363,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public async purgeDeletedSecret( + public purgeDeletedSecret( secretName: string, options: PurgeDeletedSecretOptions = {} ): Promise { - return this.tracingClient.withSpan("purgeDeletedSecret", options, async (updatedOptions) => { + return withTrace("purgeDeletedSecret", options, async (updatedOptions) => { await this.client.purgeDeletedSecret(this.vaultUrl, secretName, updatedOptions); }); } @@ -445,11 +432,11 @@ export class SecretClient { * @param secretName - The name of the secret. * @param options - The optional parameters. */ - public async backupSecret( + public backupSecret( secretName: string, options: BackupSecretOptions = {} ): Promise { - return this.tracingClient.withSpan("backupSecret", options, async (updatedOptions) => { + return withTrace("backupSecret", options, async (updatedOptions) => { const response = await this.client.backupSecret(this.vaultUrl, secretName, updatedOptions); return response.value; @@ -471,11 +458,11 @@ export class SecretClient { * @param secretBundleBackup - The backup blob associated with a secret bundle. * @param options - The optional parameters. */ - public async restoreSecretBackup( + public restoreSecretBackup( secretBundleBackup: Uint8Array, options: RestoreSecretBackupOptions = {} ): Promise { - return this.tracingClient.withSpan("restoreSecretBackup", options, async (updatedOptions) => { + return withTrace("restoreSecretBackup", options, async (updatedOptions) => { const response = await this.client.restoreSecret( this.vaultUrl, secretBundleBackup, @@ -501,7 +488,7 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await this.tracingClient.withSpan( + const currentSetResponse = await withTrace( "listPropertiesOfSecretVersions", optionsComplete, (updatedOptions) => this.client.getSecretVersions(this.vaultUrl, secretName, updatedOptions) @@ -515,7 +502,7 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await this.tracingClient.withSpan( + const currentSetResponse = await withTrace( "listPropertiesOfSecretVersions", options, (updatedOptions) => @@ -602,8 +589,8 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await this.tracingClient.withSpan( - "listPropertiesOfSecretsPage", + const currentSetResponse = await withTrace( + "listPropertiesOfSecrets", optionsComplete, (updatedOptions) => this.client.getSecrets(this.vaultUrl, updatedOptions) ); @@ -616,8 +603,8 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await this.tracingClient.withSpan( - "listPropertiesOfSecretsPage", + const currentSetResponse = await withTrace( + "listPropertiesOfSecrets", options, (updatedOptions) => this.client.getSecrets(continuationState.continuationToken!, updatedOptions) @@ -695,7 +682,7 @@ export class SecretClient { maxresults: continuationState.maxPageSize, ...options, }; - const currentSetResponse = await this.tracingClient.withSpan( + const currentSetResponse = await withTrace( "listDeletedSecrets", optionsComplete, (updatedOptions) => this.client.getDeletedSecrets(this.vaultUrl, updatedOptions) @@ -708,11 +695,8 @@ export class SecretClient { } } while (continuationState.continuationToken) { - const currentSetResponse = await this.tracingClient.withSpan( - "lisDeletedSecrets", - options, - (updatedOptions) => - this.client.getDeletedSecrets(continuationState.continuationToken!, updatedOptions) + const currentSetResponse = await withTrace("lisDeletedSecrets", options, (updatedOptions) => + this.client.getDeletedSecrets(continuationState.continuationToken!, updatedOptions) ); continuationState.continuationToken = currentSetResponse.nextLink; if (currentSetResponse.value) { diff --git a/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts b/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts index 1affe3fd190d..7c5746e583ae 100644 --- a/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts +++ b/sdk/keyvault/keyvault-secrets/src/lro/delete/operation.ts @@ -10,15 +10,12 @@ import { import { KeyVaultClient } from "../../generated/keyVaultClient"; import { getSecretFromSecretBundle } from "../../transformations"; import { OperationOptions } from "@azure/core-http"; -import { createTracingClient } from "@azure/core-tracing"; +import { createTraceFunction } from "../../../../keyvault-common/src"; /** * @internal */ -const withTrace = createTracingClient({ - namespace: "Microsoft.KeyVault.Delete", - packageName: "@azure/keyvault-secrets", -}).withSpan; +const withTrace = createTraceFunction("Azure.KeyVault.Secrets.DeleteSecretPoller"); /** * An interface representing the state of a delete secret's poll operation @@ -46,10 +43,7 @@ export class DeleteSecretPollOperation extends KeyVaultSecretPollOperation< * Sends a delete request for the given Key Vault Key's name to the Key Vault service. * Since the Key Vault Key won't be immediately deleted, we have {@link beginDeleteKey}. */ - private async deleteSecret( - name: string, - options: DeleteSecretOptions = {} - ): Promise { + private deleteSecret(name: string, options: DeleteSecretOptions = {}): Promise { return withTrace("deleteSecret", options, async (updatedOptions) => { const response = await this.client.deleteSecret(this.vaultUrl, name, updatedOptions); return getSecretFromSecretBundle(response); @@ -60,7 +54,7 @@ export class DeleteSecretPollOperation extends KeyVaultSecretPollOperation< * The getDeletedSecret method returns the specified deleted secret along with its properties. * This operation requires the secrets/get permission. */ - private async getDeletedSecret( + private getDeletedSecret( name: string, options: GetDeletedSecretOptions = {} ): Promise { diff --git a/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts b/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts index 257a21eb8efb..96cfdc6f6332 100644 --- a/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts +++ b/sdk/keyvault/keyvault-secrets/src/lro/recover/operation.ts @@ -15,17 +15,13 @@ import { import { KeyVaultClient } from "../../generated/keyVaultClient"; import { getSecretFromSecretBundle } from "../../transformations"; import { OperationOptions } from "@azure/core-http"; -import { createTracingClient } from "@azure/core-tracing"; -// import { createTraceFunction } from "../../../../keyvault-common/src"; +import { createTraceFunction } from "../../../../keyvault-common/src"; /** * @internal */ -const withTrace = createTracingClient({ - namespace: "Microsoft.KeyVault.Recover", - packageName: "@azure/keyvault-secrets", -}).withSpan; +const withTrace = createTraceFunction("Azure.KeyVault.Secrets.RecoverDeletedSecretPoller"); /** * An interface representing the state of a delete secret's poll operation @@ -53,7 +49,7 @@ export class RecoverDeletedSecretPollOperation extends KeyVaultSecretPollOperati * The getSecret method returns the specified secret along with its properties. * This operation requires the secrets/get permission. */ - private async getSecret(name: string, options: GetSecretOptions = {}): Promise { + private getSecret(name: string, options: GetSecretOptions = {}): Promise { return withTrace("getSecret", options, async (updatedOptions) => { const response = await this.client.getSecret( this.vaultUrl, @@ -69,7 +65,7 @@ export class RecoverDeletedSecretPollOperation extends KeyVaultSecretPollOperati * The recoverDeletedSecret method recovers the specified deleted secret along with its properties. * This operation requires the secrets/recover permission. */ - private async recoverDeletedSecret( + private recoverDeletedSecret( name: string, options: GetSecretOptions = {} ): Promise { diff --git a/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts b/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts index 32828e3d233d..54755de842d2 100644 --- a/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts +++ b/sdk/keyvault/keyvault-secrets/test/public/CRUD.spec.ts @@ -2,9 +2,8 @@ // Licensed under the MIT license. import { Context } from "mocha"; -import chai, { assert } from "chai"; -import { chaiAzureTrace } from "@azure/test-utils"; -chai.use(chaiAzureTrace); +import { assert } from "chai"; +import { supportsTracing } from "../../../keyvault-common/test/utils/supportsTracing"; import { env, Recorder } from "@azure-tools/test-recorder"; import { AbortController } from "@azure/abort-controller"; @@ -372,8 +371,8 @@ describe("Secret client - create, read, update and delete operations", () => { const secretName = testClient.formatName( `${secretPrefix}-${this!.test!.title}-${secretSuffix}` ); - await assert.supportsTracing( - (options) => client.setSecret(secretName, "value", options), + await supportsTracing( + (tracingOptions) => client.setSecret(secretName, "value", { tracingOptions }), ["Azure.KeyVault.Secrets.SecretClient.setSecret"] ); });