From 1b27f2a034f544fb77839c602ca806cc74669903 Mon Sep 17 00:00:00 2001 From: chradek Date: Fri, 20 Sep 2019 15:38:54 -0700 Subject: [PATCH 1/9] [Event Hubs] add tracing support when sending events --- sdk/core/core-tracing/lib/index.ts | 2 + .../lib/utils/extractSpanContext.ts | 26 ++++++++ .../core-tracing/lib/utils/getTraceParent.ts | 21 +++++++ .../core-tracing/review/core-tracing.api.md | 4 ++ sdk/eventhub/event-hubs/package.json | 1 + .../event-hubs/review/event-hubs.api.md | 14 +++-- .../event-hubs/src/diagnostics/messageSpan.ts | 32 ++++++++++ sdk/eventhub/event-hubs/src/eventDataBatch.ts | 37 ++++++++++- sdk/eventhub/event-hubs/src/eventHubClient.ts | 14 ++++- sdk/eventhub/event-hubs/src/sender.ts | 61 ++++++++++++++++++- 10 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 sdk/core/core-tracing/lib/utils/extractSpanContext.ts create mode 100644 sdk/core/core-tracing/lib/utils/getTraceParent.ts create mode 100644 sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts diff --git a/sdk/core/core-tracing/lib/index.ts b/sdk/core/core-tracing/lib/index.ts index 0d9f4812afe8..8165c45cccf2 100644 --- a/sdk/core/core-tracing/lib/index.ts +++ b/sdk/core/core-tracing/lib/index.ts @@ -35,3 +35,5 @@ export { Tracer as OpenCensusTracer, Span as OpenCensusSpan, } from "@opencensus/web-types"; +export { getTraceParent } from "./utils/getTraceParent"; +export { extractSpanContextFromTraceParent } from "./utils/extractSpanContext"; diff --git a/sdk/core/core-tracing/lib/utils/extractSpanContext.ts b/sdk/core/core-tracing/lib/utils/extractSpanContext.ts new file mode 100644 index 000000000000..b788ab1f0b00 --- /dev/null +++ b/sdk/core/core-tracing/lib/utils/extractSpanContext.ts @@ -0,0 +1,26 @@ +import { SpanContext } from '../interfaces/span_context'; + +/** + * 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 extractSpanContextFromTraceParent(traceParent: string): SpanContext { + const parts = traceParent.split("-"); + + if (parts.length !== 4) { + throw new Error(`Unable to extract span context from traceparent "${traceParent}".`); + } + + const [_, traceId, spanId, traceFlags] = parts; + + const traceOptions = parseInt(traceFlags, 16); + + const spanContext: SpanContext = { + spanId, + traceId, + traceOptions + }; + + return spanContext; +} \ No newline at end of file diff --git a/sdk/core/core-tracing/lib/utils/getTraceParent.ts b/sdk/core/core-tracing/lib/utils/getTraceParent.ts new file mode 100644 index 000000000000..3ec99f4ab56a --- /dev/null +++ b/sdk/core/core-tracing/lib/utils/getTraceParent.ts @@ -0,0 +1,21 @@ +import { SpanContext } from "../interfaces/span_context"; +import { TraceOptions } from '../interfaces/trace_options'; + +const VERSION = "00"; + +/** + * 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 getTraceParent(spanContext: SpanContext): string { + if (!spanContext.traceId || !spanContext.spanId) { + throw new Error(`Missing required fields "traceId" or "spanId" from spanContext.`); + } + + const traceOptions = spanContext.traceOptions || TraceOptions.UNSAMPLED; + const traceFlags = (traceOptions < 10) ? `0${traceOptions.toString(16)}` : traceOptions.toString(16); + + // https://www.w3.org/TR/trace-context/#traceparent-header-field-values + return `${VERSION}-${spanContext.traceId}-${spanContext.spanId}-${traceFlags}`; +} \ No newline at end of file diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 3c4599c89f46..3beeca7409be 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -56,6 +56,10 @@ export interface HttpTextFormat { extract(format: string, carrier: unknown): SpanContext | null; inject(spanContext: SpanContext, format: string, carrier: unknown): void; } +export function extractSpanContextFromTraceParent(traceParent: string): SpanContext; + +// @public +export function getTraceParent(spanContext: SpanContext): string; // @public export interface Link { diff --git a/sdk/eventhub/event-hubs/package.json b/sdk/eventhub/event-hubs/package.json index 977082780900..15c2b7adf402 100644 --- a/sdk/eventhub/event-hubs/package.json +++ b/sdk/eventhub/event-hubs/package.json @@ -65,6 +65,7 @@ "@azure/abort-controller": "1.0.0-preview.2", "@azure/core-amqp": "1.0.0-preview.4", "@azure/core-asynciterator-polyfill": "1.0.0-preview.1", + "@azure/core-tracing": "1.0.0-preview.2", "async-lock": "^1.1.3", "buffer": "^5.2.1", "debug": "^4.1.1", diff --git a/sdk/eventhub/event-hubs/review/event-hubs.api.md b/sdk/eventhub/event-hubs/review/event-hubs.api.md index ffb83887e321..f4ba05813c60 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -18,6 +18,8 @@ import { Receiver } from 'rhea-promise'; import { ReceiverOptions } from 'rhea-promise'; import { RetryOptions } from '@azure/core-amqp'; import { SharedKeyCredential } from '@azure/core-amqp'; +import { Span } from '@azure/core-tracing'; +import { SpanContext } from '@azure/core-tracing'; import { TokenCredential } from '@azure/core-amqp'; import { TokenType } from '@azure/core-amqp'; import { WebSocketImpl } from 'rhea-promise'; @@ -69,16 +71,19 @@ export class EventDataBatch { constructor(context: ConnectionContext, maxSizeInBytes: number, partitionKey?: string); readonly batchMessage: Buffer | undefined; readonly count: number; + // @internal + readonly _messageSpanContexts: SpanContext[]; readonly partitionKey: string | undefined; readonly sizeInBytes: number; - tryAdd(eventData: EventData): boolean; + // Warning: (ae-forgotten-export) The symbol "TryAddOptions" needs to be exported by the entry point index.d.ts + tryAdd(eventData: EventData, options?: TryAddOptions): boolean; } // @public export class EventHubClient { - constructor(connectionString: string, options?: EventHubClientOptions); - constructor(connectionString: string, eventHubName: string, options?: EventHubClientOptions); constructor(host: string, eventHubName: string, credential: TokenCredential, options?: EventHubClientOptions); + constructor(connectionString: string, eventHubName: string, options?: EventHubClientOptions); + constructor(connectionString: string, options?: EventHubClientOptions); close(): Promise; createConsumer(consumerGroup: string, partitionId: string, eventPosition: EventPosition, options?: EventHubConsumerOptions): EventHubConsumer; static createFromIotHubConnectionString(iothubConnectionString: string, options?: EventHubClientOptions): Promise; @@ -126,7 +131,7 @@ export interface EventHubConsumerOptions { // @public export class EventHubProducer { // @internal - constructor(context: ConnectionContext, options?: EventHubProducerOptions); + constructor(eventHubName: string, endpoint: string, context: ConnectionContext, options?: EventHubProducerOptions); close(): Promise; createBatch(options?: BatchOptions): Promise; readonly isClosed: boolean; @@ -286,6 +291,7 @@ export { RetryOptions } // @public export interface SendOptions { abortSignal?: AbortSignalLike; + parentSpan?: Span | SpanContext; partitionKey?: string | null; } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts new file mode 100644 index 000000000000..dbcb65ac9db7 --- /dev/null +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -0,0 +1,32 @@ +import { SpanContext, Span, TracerProxy, SpanKind, getTraceParent } from '@azure/core-tracing'; +import { EventData } from '../eventData'; + +export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; + +export function instrumentEventData(eventData: EventData, span: Span): boolean { + if (eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) { + return false; + } + + try { + const traceParent = getTraceParent(span.context()); + eventData.properties = eventData.properties || {}; + eventData.properties[TRACEPARENT_PROPERTY] = traceParent; + } catch { + // swallow the error, the event data won't be modified + return false; + }; + + return true; +} + +export function createMessageSpan(parentSpan?: Span | SpanContext): Span { + const tracer = TracerProxy.getTracer(); + const span = tracer.startSpan("Azure.EventHubs.message", { + kind: SpanKind.INTERNAL, + parent: parentSpan + }); + span.start(); // TODO: remove once #5182 is merged + + return span; +} \ No newline at end of file diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index a7bc00efce79..3964f9761ed0 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -6,6 +6,15 @@ import { ConnectionContext } from "./connectionContext"; import { AmqpMessage } from "@azure/core-amqp"; import { message } from "rhea-promise"; import { throwTypeErrorIfParameterMissing } from "./util/error"; +import { Span, SpanContext } from "@azure/core-tracing"; +import { instrumentEventData, TRACEPARENT_PROPERTY, createMessageSpan } from './diagnostics/messageSpan'; + +export interface TryAddOptions { + /** + * The `Span` or `SpanContext` to use as the `parent` of any spans created while adding events. + */ + parentSpan?: Span | SpanContext; +} /** * A class representing a batch of events which can be passed to the `send` method of a `EventProducer` instance. @@ -50,6 +59,10 @@ export class EventDataBatch { * @property Encoded batch message. */ private _batchMessage: Buffer | undefined; + /** + * List of 'message' span contexts. + */ + private _spanContexts: SpanContext[] = []; /** * EventDataBatch should not be constructed using `new EventDataBatch()` @@ -106,6 +119,14 @@ export class EventDataBatch { return this._batchMessage; } + /** + * Gets the "message" span contexts that were created when adding events to the batch. + * @internal + * @ignore + */ + get _messageSpanContexts(): SpanContext[] { + return this._spanContexts; + } /** * Tries to add an event data to the batch if permitted by the batch's size limit. * **NOTE**: Always remember to check the return value of this method, before calling it again @@ -114,8 +135,19 @@ export class EventDataBatch { * @param eventData An individual event data object. * @returns A boolean value indicating if the event data has been added to the batch or not. */ - public tryAdd(eventData: EventData): boolean { + public tryAdd(eventData: EventData, options: TryAddOptions = {}): boolean { throwTypeErrorIfParameterMissing(this._context.connectionId, "eventData", eventData); + + let isInstrumented = false; + // check if the event has already been instrumented + if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { + const messageSpan = createMessageSpan(options.parentSpan); + // Create a shallow copy of eventData and eventData.properties in case we add the diagnostic id to the properties. + eventData = {...eventData, properties: {...eventData.properties}}; + isInstrumented = instrumentEventData(eventData, messageSpan); + this._spanContexts.push(messageSpan.context()); + messageSpan.end(); + } // Convert EventData to AmqpMessage. const amqpMessage = toAmqpMessage(eventData, this._partitionKey); amqpMessage.body = this._context.dataTransformer.encode(eventData.body); @@ -137,6 +169,9 @@ export class EventDataBatch { // this._batchMessage will be used for final send operation if (currentSize > this._maxSizeInBytes) { this._encodedMessages.pop(); + if (isInstrumented) { + this._spanContexts.pop(); + } return false; } this._batchMessage = encodedBatchMessage; diff --git a/sdk/eventhub/event-hubs/src/eventHubClient.ts b/sdk/eventhub/event-hubs/src/eventHubClient.ts index 2b2f582b6c69..b2d0f9106a02 100644 --- a/sdk/eventhub/event-hubs/src/eventHubClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubClient.ts @@ -25,6 +25,7 @@ import { AbortSignalLike } from "@azure/abort-controller"; import { EventHubProducer } from "./sender"; import { EventHubConsumer } from "./receiver"; import { throwTypeErrorIfParameterMissing, throwErrorIfConnectionClosed } from "./util/error"; +import { SpanContext, Span } from '@azure/core-tracing'; export function getRetryAttemptTimeoutInMs(retryOptions: RetryOptions | undefined): number { const timeoutInMs = @@ -86,6 +87,10 @@ export interface SendOptions { * For example, use the @azure/abort-controller to create an `AbortSignal`. */ abortSignal?: AbortSignalLike; + /** + * The `Span` or `SpanContext` to use as the `parent` of any spans created while sending events. + */ + parentSpan?: Span | SpanContext; } /** @@ -265,6 +270,11 @@ export class EventHubClient { */ private _clientOptions: EventHubClientOptions; + /** + * The Service Bus endpoint. + */ + private _endpoint: string; + /** * @property * @readonly @@ -409,6 +419,8 @@ export class EventHubClient { ConnectionConfig.validate(config); + this._endpoint = config.endpoint; + this._clientOptions = options || {}; this._context = ConnectionContext.create(config, credential, this._clientOptions); } @@ -473,7 +485,7 @@ export class EventHubClient { options.retryOptions = this._clientOptions.retryOptions; } throwErrorIfConnectionClosed(this._context); - return new EventHubProducer(this._context, options); + return new EventHubProducer(this.eventHubName, this._endpoint, this._context, options); } /** diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index 8b7b1158210a..8162c2d22db6 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -8,6 +8,8 @@ import { ConnectionContext } from "./connectionContext"; import * as log from "./log"; import { throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing } from "./util/error"; import { EventDataBatch } from "./eventDataBatch"; +import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from '@azure/core-tracing'; +import { instrumentEventData, createMessageSpan } from './diagnostics/messageSpan'; /** * A producer responsible for sending events to an Event Hub. @@ -41,6 +43,9 @@ export class EventHubProducer { private _eventHubSender: EventHubSender | undefined; + private _eventHubName: string; + private _endpoint: string; + /** * @property Returns `true` if either the producer or the client that created it has been closed. * @readonly @@ -57,7 +62,7 @@ export class EventHubProducer { * @internal * @ignore */ - constructor(context: ConnectionContext, options?: EventHubProducerOptions) { + constructor(eventHubName: string, endpoint: string, context: ConnectionContext, options?: EventHubProducerOptions) { this._context = context; this._senderOptions = options || {}; const partitionId = @@ -65,6 +70,8 @@ export class EventHubProducer { ? String(this._senderOptions.partitionId) : undefined; this._eventHubSender = EventHubSender.create(this._context, partitionId); + this._eventHubName = eventHubName; + this._endpoint = endpoint; } /** @@ -139,7 +146,7 @@ export class EventHubProducer { */ async send( eventData: EventData | EventData[] | EventDataBatch, - options?: SendOptions + options: SendOptions = {} ): Promise { this._throwIfSenderOrConnectionClosed(); throwTypeErrorIfParameterMissing(this._context.connectionId, "eventData", eventData); @@ -154,7 +161,41 @@ export class EventHubProducer { if (!Array.isArray(eventData) && !(eventData instanceof EventDataBatch)) { eventData = [eventData]; } - return this._eventHubSender!.send(eventData, { ...this._senderOptions, ...options }); + + // link message span contexts + let spanContextsToLink: SpanContext[] = []; + if (Array.isArray(eventData)) { + for (let i = 0; i < eventData.length; i++) { + const event: EventData = {...eventData[i], properties: {...eventData[i].properties}}; + const messageSpan = createMessageSpan(options.parentSpan); + // since these message spans are created from same context as the send span, + // these message spans don't need to be linked. + instrumentEventData(event, messageSpan); + messageSpan.end(); + } + } else if (eventData instanceof EventDataBatch) { + spanContextsToLink = eventData._messageSpanContexts; + } + + + const sendSpan = this._createSendSpan(options.parentSpan); + sendSpan.start(); // TODO: remove once #5182 is merged + for (const spanContext of spanContextsToLink) { + sendSpan.addLink(spanContext); + } + + try { + const result = await this._eventHubSender!.send(eventData, { ...this._senderOptions, ...options }); + sendSpan.setStatus({code: CanonicalCode.OK}); + return result; + } catch (err) { + sendSpan.setStatus({ + code: CanonicalCode.UNKNOWN, + message: err.message + }); + } finally { + sendSpan.end(); + } } /** @@ -183,6 +224,20 @@ export class EventHubProducer { } } + private _createSendSpan(parentSpan?: Span | SpanContext): Span { + const tracer = TracerProxy.getTracer(); + const span = tracer.startSpan("Azure.EventHubs.send", { + kind: SpanKind.PRODUCER, + parent: parentSpan + }); + + span.setAttribute("component", "eventhubs"); + span.setAttribute("message_bus.destination", this._eventHubName); + span.setAttribute("peer.address", this._endpoint); + + return span; + } + private _throwIfSenderOrConnectionClosed(): void { throwErrorIfConnectionClosed(this._context); if (this.isClosed) { From b6118eb0cb93535890d45c9ee3b0facec1d66396 Mon Sep 17 00:00:00 2001 From: chradek Date: Tue, 24 Sep 2019 09:44:19 -0700 Subject: [PATCH 2/9] refactor after rebase --- .../lib/utils/extractSpanContext.ts | 6 +- .../core-tracing/lib/utils/getTraceParent.ts | 6 +- .../core-tracing/review/core-tracing.api.md | 276 +++++++++--------- sdk/eventhub/event-hubs/package.json | 2 +- .../event-hubs/src/diagnostics/messageSpan.ts | 1 - sdk/eventhub/event-hubs/src/sender.ts | 1 - 6 files changed, 147 insertions(+), 145 deletions(-) diff --git a/sdk/core/core-tracing/lib/utils/extractSpanContext.ts b/sdk/core/core-tracing/lib/utils/extractSpanContext.ts index b788ab1f0b00..5ca420373d0a 100644 --- a/sdk/core/core-tracing/lib/utils/extractSpanContext.ts +++ b/sdk/core/core-tracing/lib/utils/extractSpanContext.ts @@ -12,14 +12,14 @@ export function extractSpanContextFromTraceParent(traceParent: string): SpanCont throw new Error(`Unable to extract span context from traceparent "${traceParent}".`); } - const [_, traceId, spanId, traceFlags] = parts; + const [_, traceId, spanId, traceOptions] = parts; - const traceOptions = parseInt(traceFlags, 16); + const traceFlags = parseInt(traceOptions, 16); const spanContext: SpanContext = { spanId, traceId, - traceOptions + traceFlags }; return spanContext; diff --git a/sdk/core/core-tracing/lib/utils/getTraceParent.ts b/sdk/core/core-tracing/lib/utils/getTraceParent.ts index 3ec99f4ab56a..5cf04bf45cc0 100644 --- a/sdk/core/core-tracing/lib/utils/getTraceParent.ts +++ b/sdk/core/core-tracing/lib/utils/getTraceParent.ts @@ -1,5 +1,5 @@ import { SpanContext } from "../interfaces/span_context"; -import { TraceOptions } from '../interfaces/trace_options'; +import { TraceFlags } from '../interfaces/trace_flags'; const VERSION = "00"; @@ -13,8 +13,8 @@ export function getTraceParent(spanContext: SpanContext): string { throw new Error(`Missing required fields "traceId" or "spanId" from spanContext.`); } - const traceOptions = spanContext.traceOptions || TraceOptions.UNSAMPLED; - const traceFlags = (traceOptions < 10) ? `0${traceOptions.toString(16)}` : traceOptions.toString(16); + const flags = spanContext.traceFlags || TraceFlags.UNSAMPLED; + const traceFlags = (flags < 10) ? `0${flags.toString(16)}` : flags.toString(16); // 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/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 3beeca7409be..7bdb1639fda5 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -3,130 +3,129 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - -import { Span as OpenCensusSpan } from '@opencensus/web-types'; -import { Tracer as OpenCensusTracer } from '@opencensus/web-types'; +import { Span as OpenCensusSpan } from "@opencensus/web-types"; +import { Tracer as OpenCensusTracer } from "@opencensus/web-types"; // @public export interface Attributes { - [attributeKey: string]: unknown; + [attributeKey: string]: unknown; } // @public export interface BinaryFormat { - fromBytes(buffer: ArrayBuffer): SpanContext | null; - toBytes(spanContext: SpanContext): ArrayBuffer; + fromBytes(buffer: ArrayBuffer): SpanContext | null; + toBytes(spanContext: SpanContext): ArrayBuffer; } // @public export enum CanonicalCode { - ABORTED = 10, - ALREADY_EXISTS = 6, - CANCELLED = 1, - DATA_LOSS = 15, - DEADLINE_EXCEEDED = 4, - FAILED_PRECONDITION = 9, - INTERNAL = 13, - INVALID_ARGUMENT = 3, - NOT_FOUND = 5, - OK = 0, - OUT_OF_RANGE = 11, - PERMISSION_DENIED = 7, - RESOURCE_EXHAUSTED = 8, - UNAUTHENTICATED = 16, - UNAVAILABLE = 14, - UNIMPLEMENTED = 12, - UNKNOWN = 2 + ABORTED = 10, + ALREADY_EXISTS = 6, + CANCELLED = 1, + DATA_LOSS = 15, + DEADLINE_EXCEEDED = 4, + FAILED_PRECONDITION = 9, + INTERNAL = 13, + INVALID_ARGUMENT = 3, + NOT_FOUND = 5, + OK = 0, + OUT_OF_RANGE = 11, + PERMISSION_DENIED = 7, + RESOURCE_EXHAUSTED = 8, + UNAUTHENTICATED = 16, + UNAVAILABLE = 14, + UNIMPLEMENTED = 12, + UNKNOWN = 2 } // @public export interface Event { - attributes?: Attributes; - name: string; + attributes?: Attributes; + name: string; } // @public export function getTracer(): Tracer; +export function extractSpanContextFromTraceParent(traceParent: string): SpanContext; + +// @public +export function getTraceParent(spanContext: SpanContext): string; // @public export type HrTime = [number, number]; // @public export interface HttpTextFormat { - extract(format: string, carrier: unknown): SpanContext | null; - inject(spanContext: SpanContext, format: string, carrier: unknown): void; + extract(format: string, carrier: unknown): SpanContext | null; + inject(spanContext: SpanContext, format: string, carrier: unknown): void; } -export function extractSpanContextFromTraceParent(traceParent: string): SpanContext; - -// @public -export function getTraceParent(spanContext: SpanContext): string; // @public export interface Link { - attributes?: Attributes; - spanContext: SpanContext; + attributes?: Attributes; + spanContext: SpanContext; } // @public export class NoOpSpan implements Span { - addEvent(_name: string, _attributes?: Attributes): this; - addLink(_spanContext: SpanContext, _attributes?: Attributes): this; - context(): SpanContext; - end(_endTime?: number): void; - isRecordingEvents(): boolean; - setAttribute(_key: string, _value: unknown): this; - setAttributes(_attributes: Attributes): this; - setStatus(_status: Status): this; - updateName(_name: string): this; + addEvent(_name: string, _attributes?: Attributes): this; + addLink(_spanContext: SpanContext, _attributes?: Attributes): this; + context(): SpanContext; + end(_endTime?: number): void; + isRecordingEvents(): boolean; + setAttribute(_key: string, _value: unknown): this; + setAttributes(_attributes: Attributes): this; + setStatus(_status: Status): this; + updateName(_name: string): this; } // @public export class NoOpTracer implements Tracer { - bind(target: T, _span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span; - getHttpTextFormat(): HttpTextFormat; - recordSpanData(_span: Span): void; - startSpan(_name: string, _options?: SpanOptions): Span; - withSpan ReturnType>(_span: Span, fn: T): ReturnType; + bind(target: T, _span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span; + getHttpTextFormat(): HttpTextFormat; + recordSpanData(_span: Span): void; + startSpan(_name: string, _options?: SpanOptions): Span; + withSpan ReturnType>(_span: Span, fn: T): ReturnType; } -export { OpenCensusSpan } +export { OpenCensusSpan }; // @public export class OpenCensusSpanWrapper implements Span { - constructor(tracer: OpenCensusTracerWrapper, name: string, options?: SpanOptions); - addEvent(name: string, attributes?: Attributes): this; - addLink(spanContext: SpanContext, attributes?: Attributes): this; - context(): SpanContext; - end(_endTime?: number): void; - getWrappedSpan(): OpenCensusSpan; - isRecordingEvents(): boolean; - setAttribute(key: string, value: unknown): this; - setAttributes(attributes: Attributes): this; - setStatus(status: Status): this; - updateName(name: string): this; + constructor(tracer: OpenCensusTracerWrapper, name: string, options?: SpanOptions); + addEvent(name: string, attributes?: Attributes): this; + addLink(spanContext: SpanContext, attributes?: Attributes): this; + context(): SpanContext; + end(_endTime?: number): void; + getWrappedSpan(): OpenCensusSpan; + isRecordingEvents(): boolean; + setAttribute(key: string, value: unknown): this; + setAttributes(attributes: Attributes): this; + setStatus(status: Status): this; + updateName(name: string): this; } -export { OpenCensusTracer } +export { OpenCensusTracer }; // @public export class OpenCensusTracerWrapper implements Tracer { - constructor(tracer: OpenCensusTracer); - bind(target: T, span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span | null; - getHttpTextFormat(): HttpTextFormat; - getWrappedTracer(): OpenCensusTracer; - recordSpanData(span: Span): void; - startSpan(name: string, options?: SpanOptions): Span; - withSpan unknown>(span: Span, fn: T): ReturnType; + constructor(tracer: OpenCensusTracer); + bind(target: T, span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span | null; + getHttpTextFormat(): HttpTextFormat; + getWrappedTracer(): OpenCensusTracer; + recordSpanData(span: Span): void; + startSpan(name: string, options?: SpanOptions): Span; + withSpan unknown>(span: Span, fn: T): ReturnType; } // @public export interface Sampler { - shouldSample(parentContext?: SpanContext): boolean; - toString(): string; + shouldSample(parentContext?: SpanContext): boolean; + toString(): string; } // @public @@ -134,88 +133,95 @@ export function setTracer(tracer: Tracer): void; // @public export interface Span { - addEvent(name: string, attributes?: Attributes): this; - addLink(spanContext: SpanContext, attributes?: Attributes): this; - context(): SpanContext; - end(endTime?: TimeInput): void; - isRecordingEvents(): boolean; - setAttribute(key: string, value: unknown): this; - setAttributes(attributes: Attributes): this; - setStatus(status: Status): this; - updateName(name: string): this; + addEvent(name: string, attributes?: Attributes): this; + addLink(spanContext: SpanContext, attributes?: Attributes): this; + context(): SpanContext; + end(endTime?: TimeInput): void; + isRecordingEvents(): boolean; + setAttribute(key: string, value: unknown): this; + setAttributes(attributes: Attributes): this; + setStatus(status: Status): this; + updateName(name: string): this; } // @public export interface SpanContext { - spanId: string; - traceFlags?: TraceFlags; - traceId: string; - traceState?: TraceState; + spanId: string; + traceFlags?: TraceFlags; + traceId: string; + traceState?: TraceState; } // @public export interface SpanGraph { - roots: SpanGraphNode[]; + roots: SpanGraphNode[]; } // @public export interface SpanGraphNode { - children: SpanGraphNode[]; - name: string; + children: SpanGraphNode[]; + name: string; } // @public export enum SpanKind { - CLIENT = 2, - CONSUMER = 4, - INTERNAL = 0, - PRODUCER = 3, - SERVER = 1 + CLIENT = 2, + CONSUMER = 4, + INTERNAL = 0, + PRODUCER = 3, + SERVER = 1 } // @public export interface SpanOptions { - attributes?: Attributes; - isRecordingEvents?: boolean; - kind?: SpanKind; - parent?: Span | SpanContext; - startTime?: number; + attributes?: Attributes; + isRecordingEvents?: boolean; + kind?: SpanKind; + parent?: Span | SpanContext; + startTime?: number; } // @public export interface Status { - code: CanonicalCode; - message?: string; + code: CanonicalCode; + message?: string; } // @public export class TestSpan extends NoOpSpan { - constructor(parentTracer: TestTracer, name: string, context: SpanContext, kind: SpanKind, parentSpanId?: string, startTime?: TimeInput); - context(): SpanContext; - end(_endTime?: number): void; - endCalled: boolean; - isRecordingEvents(): boolean; - kind: SpanKind; - name: string; - readonly parentSpanId?: string; - setStatus(status: Status): this; - readonly startTime: TimeInput; - status: Status; - tracer(): Tracer; - } + constructor( + parentTracer: TestTracer, + name: string, + context: SpanContext, + kind: SpanKind, + parentSpanId?: string, + startTime?: TimeInput + ); + context(): SpanContext; + end(_endTime?: number): void; + endCalled: boolean; + isRecordingEvents(): boolean; + kind: SpanKind; + name: string; + readonly parentSpanId?: string; + setStatus(status: Status): this; + readonly startTime: TimeInput; + status: Status; + tracer(): Tracer; +} // @public export class TestTracer extends NoOpTracer { - getActiveSpans(): TestSpan[]; - getKnownSpans(): TestSpan[]; - getRootSpans(): TestSpan[]; - getSpanGraph(traceId: string): SpanGraph; - startSpan(name: string, options?: SpanOptions): TestSpan; - } + getActiveSpans(): TestSpan[]; + getKnownSpans(): TestSpan[]; + getRootSpans(): TestSpan[]; + getSpanGraph(traceId: string): SpanGraph; + startSpan(name: string, options?: SpanOptions): TestSpan; +} // @public export interface TimedEvent extends Event { - time: HrTime; + time: HrTime; } // @public @@ -223,30 +229,28 @@ export type TimeInput = HrTime | number | Date; // @public export enum TraceFlags { - SAMPLED = 1, - UNSAMPLED = 0 + SAMPLED = 1, + UNSAMPLED = 0 } // @public export interface Tracer { - bind(target: T, span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span | null; - getHttpTextFormat(): HttpTextFormat; - recordSpanData(span: Span): void; - startSpan(name: string, options?: SpanOptions): Span; - withSpan ReturnType>(span: Span, fn: T): ReturnType; + bind(target: T, span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span | null; + getHttpTextFormat(): HttpTextFormat; + recordSpanData(span: Span): void; + startSpan(name: string, options?: SpanOptions): Span; + withSpan ReturnType>(span: Span, fn: T): ReturnType; } // @public export interface TraceState { - get(key: string): string | undefined; - serialize(): string; - set(key: string, value: string): void; - unset(key: string): void; + get(key: string): string | undefined; + serialize(): string; + set(key: string, value: string): void; + unset(key: string): void; } - // (No @packageDocumentation comment for this package) - ``` diff --git a/sdk/eventhub/event-hubs/package.json b/sdk/eventhub/event-hubs/package.json index 15c2b7adf402..184403b8a83f 100644 --- a/sdk/eventhub/event-hubs/package.json +++ b/sdk/eventhub/event-hubs/package.json @@ -65,7 +65,7 @@ "@azure/abort-controller": "1.0.0-preview.2", "@azure/core-amqp": "1.0.0-preview.4", "@azure/core-asynciterator-polyfill": "1.0.0-preview.1", - "@azure/core-tracing": "1.0.0-preview.2", + "@azure/core-tracing": "1.0.0-preview.3", "async-lock": "^1.1.3", "buffer": "^5.2.1", "debug": "^4.1.1", diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts index dbcb65ac9db7..bbcfef3c2bb5 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -26,7 +26,6 @@ export function createMessageSpan(parentSpan?: Span | SpanContext): Span { kind: SpanKind.INTERNAL, parent: parentSpan }); - span.start(); // TODO: remove once #5182 is merged return span; } \ No newline at end of file diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index 8162c2d22db6..289b9d287bbc 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -179,7 +179,6 @@ export class EventHubProducer { const sendSpan = this._createSendSpan(options.parentSpan); - sendSpan.start(); // TODO: remove once #5182 is merged for (const spanContext of spanContextsToLink) { sendSpan.addLink(spanContext); } From 3def75aa5bd7053efe20b4e0648d88188c14d605 Mon Sep 17 00:00:00 2001 From: chradek Date: Tue, 24 Sep 2019 13:50:49 -0700 Subject: [PATCH 3/9] adds tracing support to eventHubClient operations --- .../event-hubs/review/event-hubs.api.md | 36 +++++- .../src/diagnostics/instrumentEventData.ts | 21 +++ .../event-hubs/src/diagnostics/messageSpan.ts | 22 +--- sdk/eventhub/event-hubs/src/eventDataBatch.ts | 7 +- sdk/eventhub/event-hubs/src/eventHubClient.ts | 122 +++++++++++++++--- sdk/eventhub/event-hubs/src/index.ts | 9 +- sdk/eventhub/event-hubs/src/sender.ts | 3 +- .../event-hubs/test/hubruntime.spec.ts | 12 +- 8 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts diff --git a/sdk/eventhub/event-hubs/review/event-hubs.api.md b/sdk/eventhub/event-hubs/review/event-hubs.api.md index f4ba05813c60..c3540b73029d 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -24,6 +24,11 @@ import { TokenCredential } from '@azure/core-amqp'; import { TokenType } from '@azure/core-amqp'; import { WebSocketImpl } from 'rhea-promise'; +// @public +export interface AbortSignalOptions { + abortSignal?: AbortSignalLike; +} + // @public export interface BatchOptions { abortSignal?: AbortSignalLike; @@ -75,15 +80,14 @@ export class EventDataBatch { readonly _messageSpanContexts: SpanContext[]; readonly partitionKey: string | undefined; readonly sizeInBytes: number; - // Warning: (ae-forgotten-export) The symbol "TryAddOptions" needs to be exported by the entry point index.d.ts tryAdd(eventData: EventData, options?: TryAddOptions): boolean; } // @public export class EventHubClient { constructor(host: string, eventHubName: string, credential: TokenCredential, options?: EventHubClientOptions); - constructor(connectionString: string, eventHubName: string, options?: EventHubClientOptions); constructor(connectionString: string, options?: EventHubClientOptions); + constructor(connectionString: string, eventHubName: string, options?: EventHubClientOptions); close(): Promise; createConsumer(consumerGroup: string, partitionId: string, eventPosition: EventPosition, options?: EventHubConsumerOptions): EventHubConsumer; static createFromIotHubConnectionString(iothubConnectionString: string, options?: EventHubClientOptions): Promise; @@ -91,9 +95,9 @@ export class EventHubClient { static defaultConsumerGroupName: string; readonly eventHubName: string; readonly fullyQualifiedNamespace: string; - getPartitionIds(abortSignal?: AbortSignalLike): Promise>; - getPartitionProperties(partitionId: string, abortSignal?: AbortSignalLike): Promise; - getProperties(abortSignal?: AbortSignalLike): Promise; + getPartitionIds(options?: GetPartitionIdsOptions): Promise>; + getPartitionProperties(partitionId: string, options?: GetPartitionPropertiesOptions): Promise; + getProperties(options?: GetPropertiesOptions): Promise; } // @public @@ -188,6 +192,18 @@ export interface EventProcessorOptions { trackLastEnqueuedEventInfo?: boolean; } +// @public +export interface GetPartitionIdsOptions extends AbortSignalOptions, ParentSpanOptions { +} + +// @public +export interface GetPartitionPropertiesOptions extends AbortSignalOptions, ParentSpanOptions { +} + +// @public +export interface GetPropertiesOptions extends AbortSignalOptions, ParentSpanOptions { +} + // @public export class InMemoryPartitionManager implements PartitionManager { claimOwnership(partitionOwnership: PartitionOwnership[]): Promise; @@ -211,6 +227,11 @@ export type OnError = (error: MessagingError | Error) => void; // @public export type OnMessage = (eventData: ReceivedEventData) => void; +// @public +export interface ParentSpanOptions { + parentSpan?: Span | SpanContext; +} + // @public export interface PartitionManager { claimOwnership(partitionOwnership: PartitionOwnership[]): Promise; @@ -299,6 +320,11 @@ export { TokenCredential } export { TokenType } +// @public +export interface TryAddOptions { + parentSpan?: Span | SpanContext; +} + export { WebSocketImpl } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts new file mode 100644 index 000000000000..1094530e02ca --- /dev/null +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -0,0 +1,21 @@ +import { Span, getTraceParent } from '@azure/core-tracing'; +import { EventData } from '../eventData'; + +export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; + +export function instrumentEventData(eventData: EventData, span: Span): boolean { + if (eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) { + return false; + } + + try { + const traceParent = getTraceParent(span.context()); + eventData.properties = eventData.properties || {}; + eventData.properties[TRACEPARENT_PROPERTY] = traceParent; + } catch { + // swallow the error, the event data won't be modified + return false; + }; + + return true; +} \ No newline at end of file diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts index bbcfef3c2bb5..6a0af40deea3 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -1,24 +1,4 @@ -import { SpanContext, Span, TracerProxy, SpanKind, getTraceParent } from '@azure/core-tracing'; -import { EventData } from '../eventData'; - -export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; - -export function instrumentEventData(eventData: EventData, span: Span): boolean { - if (eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) { - return false; - } - - try { - const traceParent = getTraceParent(span.context()); - eventData.properties = eventData.properties || {}; - eventData.properties[TRACEPARENT_PROPERTY] = traceParent; - } catch { - // swallow the error, the event data won't be modified - return false; - }; - - return true; -} +import { SpanContext, Span, TracerProxy, SpanKind } from '@azure/core-tracing'; export function createMessageSpan(parentSpan?: Span | SpanContext): Span { const tracer = TracerProxy.getTracer(); diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 3964f9761ed0..3b31f78a0773 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -7,8 +7,13 @@ import { AmqpMessage } from "@azure/core-amqp"; import { message } from "rhea-promise"; import { throwTypeErrorIfParameterMissing } from "./util/error"; import { Span, SpanContext } from "@azure/core-tracing"; -import { instrumentEventData, TRACEPARENT_PROPERTY, createMessageSpan } from './diagnostics/messageSpan'; +import { instrumentEventData, TRACEPARENT_PROPERTY } from "./diagnostics/instrumentEventData"; +import { createMessageSpan } from './diagnostics/messageSpan'; +/** + * The set of options to configure the behavior of `tryAdd`. + * - `parentSpan` : The `Span` or `SpanContext` to use as the `parent` of the span created while calling this operation. + */ export interface TryAddOptions { /** * The `Span` or `SpanContext` to use as the `parent` of any spans created while adding events. diff --git a/sdk/eventhub/event-hubs/src/eventHubClient.ts b/sdk/eventhub/event-hubs/src/eventHubClient.ts index b2d0f9106a02..1a55678b5b0e 100644 --- a/sdk/eventhub/event-hubs/src/eventHubClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubClient.ts @@ -25,8 +25,14 @@ import { AbortSignalLike } from "@azure/abort-controller"; import { EventHubProducer } from "./sender"; import { EventHubConsumer } from "./receiver"; import { throwTypeErrorIfParameterMissing, throwErrorIfConnectionClosed } from "./util/error"; -import { SpanContext, Span } from '@azure/core-tracing'; +import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from '@azure/core-tracing'; +type OperationNames = "getProperties"|"getPartitionIds"|"getPartitionProperties"; + +/** + * @internal + * @ignore + */ export function getRetryAttemptTimeoutInMs(retryOptions: RetryOptions | undefined): number { const timeoutInMs = retryOptions == undefined || @@ -38,6 +44,50 @@ export function getRetryAttemptTimeoutInMs(retryOptions: RetryOptions | undefine return timeoutInMs; } +/** + * The set of options to configure request cancellation. + * - `abortSignal` : A signal used to cancel an asynchronous operation. + */ +export interface AbortSignalOptions { + /** + * An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * For example, use the @azure/abort-controller to create an `AbortSignal`. + */ + abortSignal?: AbortSignalLike; +} + +/** + * The set of options to manually propagate `Span` context for distributed tracing. + * - `parentSpan` : The `Span` or `SpanContext` for the operation to use as a `parent` when creating its own span. + */ +export interface ParentSpanOptions { + /** + * The `Span` or `SpanContext` to use as the `parent` of any spans created while calling operations that make a request to the service. + */ + parentSpan?: Span | SpanContext; +} + +/** + * The set of options to configure the behavior of `getProperties`. + * - `abortSignal` : An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * - `parentSpan` : The `Span` or `SpanContext` to use as the `parent` of the span created while calling this operation. + */ +export interface GetPropertiesOptions extends AbortSignalOptions, ParentSpanOptions {} + +/** + * The set of options to configure the behavior of `getPartitionProperties`. + * - `abortSignal` : An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * - `parentSpan` : The `Span` or `SpanContext` to use as the `parent` of the span created while calling this operation. + */ +export interface GetPartitionPropertiesOptions extends AbortSignalOptions, ParentSpanOptions {} + +/** + * The set of options to configure the behavior of `getPartitionIds`. + * - `abortSignal` : An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * - `parentSpan` : The `Span` or `SpanContext` to use as the `parent` of the span created while calling this operation. + */ +export interface GetPartitionIdsOptions extends AbortSignalOptions, ParentSpanOptions {} + /** * The set of options to configure the behavior of an `EventHubProducer`. * These can be specified when creating the producer via the `createProducer` method. @@ -64,7 +114,7 @@ export interface EventHubProducerOptions { /** * The set of options to configure the `send` operation on the `EventHubProducer`. * - `partitionKey` : A value that is hashed to produce a partition assignment. - * - `abortSignal` : A signal the request to cancel the send operation. + * - `abortSignal` : A signal used to cancel the send operation. * * Example usage: * ```js @@ -425,6 +475,20 @@ export class EventHubClient { this._context = ConnectionContext.create(config, credential, this._clientOptions); } + private _createClientSpan(operationName: OperationNames, parentSpan?: Span | SpanContext): Span { + const tracer = TracerProxy.getTracer(); + const span = tracer.startSpan(`Azure.EventHubs.${operationName}`, { + kind: SpanKind.CLIENT, + parent: parentSpan + }); + + span.setAttribute("component", "eventhubs"); + span.setAttribute("message_bus.destination", this.eventHubName); + span.setAttribute("peer.address", this._endpoint); + + return span; + } + /** * Closes the AMQP connection to the Event Hub instance, * returning a promise that will be resolved when disconnection is completed. @@ -539,68 +603,94 @@ export class EventHubClient { /** * Provides the Event Hub runtime information. - * @param abortSignal An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. - * For example, use the @azure/abort-controller to create an `AbortSignal`. + * @param [options] The set of options to apply to the operation call. * @returns A promise that resolves with EventHubProperties. * @throws {Error} Thrown if the underlying connection has been closed, create a new EventHubClient. * @throws {AbortError} Thrown if the operation is cancelled via the abortSignal. */ - async getProperties(abortSignal?: AbortSignalLike): Promise { + async getProperties(options: GetPropertiesOptions = {}): Promise { throwErrorIfConnectionClosed(this._context); + const clientSpan = this._createClientSpan("getProperties", options.parentSpan); try { - return await this._context.managementSession!.getHubRuntimeInformation({ + const result = await this._context.managementSession!.getHubRuntimeInformation({ retryOptions: this._clientOptions.retryOptions, - abortSignal + abortSignal: options.abortSignal }); + clientSpan.setStatus({ code: CanonicalCode.OK }); + return result; } catch (err) { + clientSpan.setStatus({ + code: CanonicalCode.UNKNOWN, + message: err.message + }); log.error("An error occurred while getting the hub runtime information: %O", err); throw err; + } finally { + clientSpan.end(); } } /** * Provides an array of partitionIds. - * @param abortSignal An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. - * For example, use the @azure/abort-controller to create an `AbortSignal`. + * @param [options] The set of options to apply to the operation call. * @returns A promise that resolves with an Array of strings. * @throws {Error} Thrown if the underlying connection has been closed, create a new EventHubClient. * @throws {AbortError} Thrown if the operation is cancelled via the abortSignal. */ - async getPartitionIds(abortSignal?: AbortSignalLike): Promise> { + async getPartitionIds(options: GetPartitionIdsOptions = {}): Promise> { throwErrorIfConnectionClosed(this._context); + const clientSpan = this._createClientSpan("getPartitionIds", options.parentSpan); try { - const runtimeInfo = await this.getProperties(abortSignal); + const runtimeInfo = await this.getProperties({ + ...options, + parentSpan: clientSpan + }); + clientSpan.setStatus({ code: CanonicalCode.OK }); return runtimeInfo.partitionIds; } catch (err) { + clientSpan.setStatus({ + code: CanonicalCode.UNKNOWN, + message: err.message + }); log.error("An error occurred while getting the partition ids: %O", err); throw err; + } finally { + clientSpan.end(); } } /** * Provides information about the specified partition. * @param partitionId Partition ID for which partition information is required. - * @param abortSignal An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. - * For example, use the @azure/abort-controller to create an `AbortSignal`. + * @param [options] The set of options to apply to the operation call. * @returns A promise that resoloves with PartitionProperties. * @throws {Error} Thrown if the underlying connection has been closed, create a new EventHubClient. * @throws {AbortError} Thrown if the operation is cancelled via the abortSignal. */ async getPartitionProperties( partitionId: string, - abortSignal?: AbortSignalLike + options: GetPartitionPropertiesOptions = {} ): Promise { throwErrorIfConnectionClosed(this._context); throwTypeErrorIfParameterMissing(this._context.connectionId, "partitionId", partitionId); partitionId = String(partitionId); + const clientSpan = this._createClientSpan("getPartitionProperties", options.parentSpan); try { - return await this._context.managementSession!.getPartitionProperties(partitionId, { + const result = await this._context.managementSession!.getPartitionProperties(partitionId, { retryOptions: this._clientOptions.retryOptions, - abortSignal + abortSignal: options.abortSignal }); + clientSpan.setStatus({ code: CanonicalCode.OK }); + return result; } catch (err) { + clientSpan.setStatus({ + code: CanonicalCode.UNKNOWN, + message: err.message + }); log.error("An error occurred while getting the partition information: %O", err); throw err; + } finally { + clientSpan.end(); } } diff --git a/sdk/eventhub/event-hubs/src/index.ts b/sdk/eventhub/event-hubs/src/index.ts index 58f08286a827..2eeab1c1d60a 100644 --- a/sdk/eventhub/event-hubs/src/index.ts +++ b/sdk/eventhub/event-hubs/src/index.ts @@ -8,18 +8,23 @@ export { WebSocketImpl } from "rhea-promise"; export { OnMessage, OnError, LastEnqueuedEventInfo } from "./eventHubReceiver"; export { ReceiveHandler } from "./receiveHandler"; export { + AbortSignalOptions, EventHubClient, EventHubClientOptions, EventHubConsumerOptions, EventHubProducerOptions, SendOptions, - BatchOptions + BatchOptions, + GetPartitionIdsOptions, + GetPartitionPropertiesOptions, + GetPropertiesOptions, + ParentSpanOptions } from "./eventHubClient"; export { EventPosition } from "./eventPosition"; export { PartitionProperties, EventHubProperties } from "./managementClient"; export { EventHubProducer } from "./sender"; export { EventHubConsumer, EventIteratorOptions } from "./receiver"; -export { EventDataBatch } from "./eventDataBatch"; +export { EventDataBatch, TryAddOptions } from "./eventDataBatch"; export { EventProcessor, CloseReason, diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index 289b9d287bbc..fe7dd62e4f14 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -9,7 +9,8 @@ import * as log from "./log"; import { throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing } from "./util/error"; import { EventDataBatch } from "./eventDataBatch"; import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from '@azure/core-tracing'; -import { instrumentEventData, createMessageSpan } from './diagnostics/messageSpan'; +import { instrumentEventData } from "./diagnostics/instrumentEventData"; +import { createMessageSpan } from './diagnostics/messageSpan'; /** * A producer responsible for sending events to an Event Hub. diff --git a/sdk/eventhub/event-hubs/test/hubruntime.spec.ts b/sdk/eventhub/event-hubs/test/hubruntime.spec.ts index 2504f353c2c1..7464a7a80e15 100644 --- a/sdk/eventhub/event-hubs/test/hubruntime.spec.ts +++ b/sdk/eventhub/event-hubs/test/hubruntime.spec.ts @@ -51,7 +51,9 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { try { const controller = new AbortController(); setTimeout(() => controller.abort(), 1); - await client.getPartitionIds(controller.signal); + await client.getPartitionIds({ + abortSignal: controller.signal + }); throw new Error(`Test failure`); } catch (err) { err.message.should.match(/The [\w]+ operation has been cancelled by the user.$/gi); @@ -94,7 +96,9 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { try { const controller = new AbortController(); setTimeout(() => controller.abort(), 1); - await client.getProperties(controller.signal); + await client.getProperties({ + abortSignal: controller.signal + }); throw new Error(`Test failure`); } catch (err) { err.message.should.match(/The [\w]+ operation has been cancelled by the user.$/gi); @@ -162,7 +166,9 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { try { const controller = new AbortController(); setTimeout(() => controller.abort(), 1); - await client.getPartitionProperties("0", controller.signal); + await client.getPartitionProperties("0", { + abortSignal: controller.signal + }); throw new Error(`Test failure`); } catch (err) { err.message.should.match(/The [\w]+ operation has been cancelled by the user.$/gi); From 1f528693abaca30a89c21eb4512567a3b5e0fcda Mon Sep 17 00:00:00 2001 From: chradek Date: Tue, 24 Sep 2019 13:53:30 -0700 Subject: [PATCH 4/9] npm format --- .../src/diagnostics/instrumentEventData.ts | 10 ++++----- .../event-hubs/src/diagnostics/messageSpan.ts | 4 ++-- sdk/eventhub/event-hubs/src/eventDataBatch.ts | 12 +++++------ sdk/eventhub/event-hubs/src/eventHubClient.ts | 8 +++---- sdk/eventhub/event-hubs/src/sender.ts | 21 ++++++++++++------- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index 1094530e02ca..af2ff47fbc19 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -1,5 +1,5 @@ -import { Span, getTraceParent } from '@azure/core-tracing'; -import { EventData } from '../eventData'; +import { Span, getTraceParent } from "@azure/core-tracing"; +import { EventData } from "../eventData"; export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; @@ -11,11 +11,11 @@ export function instrumentEventData(eventData: EventData, span: Span): boolean { try { const traceParent = getTraceParent(span.context()); eventData.properties = eventData.properties || {}; - eventData.properties[TRACEPARENT_PROPERTY] = traceParent; + eventData.properties[TRACEPARENT_PROPERTY] = traceParent; } catch { // swallow the error, the event data won't be modified return false; - }; + } return true; -} \ No newline at end of file +} diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts index 6a0af40deea3..16d4aa3f6ff6 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -1,4 +1,4 @@ -import { SpanContext, Span, TracerProxy, SpanKind } from '@azure/core-tracing'; +import { SpanContext, Span, TracerProxy, SpanKind } from "@azure/core-tracing"; export function createMessageSpan(parentSpan?: Span | SpanContext): Span { const tracer = TracerProxy.getTracer(); @@ -8,4 +8,4 @@ export function createMessageSpan(parentSpan?: Span | SpanContext): Span { }); return span; -} \ No newline at end of file +} diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 3b31f78a0773..0ff69165b7df 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -8,7 +8,7 @@ import { message } from "rhea-promise"; import { throwTypeErrorIfParameterMissing } from "./util/error"; import { Span, SpanContext } from "@azure/core-tracing"; import { instrumentEventData, TRACEPARENT_PROPERTY } from "./diagnostics/instrumentEventData"; -import { createMessageSpan } from './diagnostics/messageSpan'; +import { createMessageSpan } from "./diagnostics/messageSpan"; /** * The set of options to configure the behavior of `tryAdd`. @@ -85,7 +85,7 @@ export class EventDataBatch { } /** - * @property The partitionKey set during `EventDataBatch` creation. This value is hashed to + * @property The partitionKey set during `EventDataBatch` creation. This value is hashed to * produce a partition assignment when the producer is created without a `partitionId` * @readonly */ @@ -113,9 +113,9 @@ export class EventDataBatch { /** * @property Represents the single AMQP message which is the result of encoding all the events * added into the `EventDataBatch` instance. - * + * * This is not meant for the user to use directly. - * + * * When the `EventDataBatch` instance is passed to the `send()` method on the `EventHubProducer`, * this single batched AMQP message is what gets sent over the wire to the service. * @readonly @@ -136,7 +136,7 @@ export class EventDataBatch { * Tries to add an event data to the batch if permitted by the batch's size limit. * **NOTE**: Always remember to check the return value of this method, before calling it again * for the next event. - * + * * @param eventData An individual event data object. * @returns A boolean value indicating if the event data has been added to the batch or not. */ @@ -148,7 +148,7 @@ export class EventDataBatch { if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { const messageSpan = createMessageSpan(options.parentSpan); // Create a shallow copy of eventData and eventData.properties in case we add the diagnostic id to the properties. - eventData = {...eventData, properties: {...eventData.properties}}; + eventData = { ...eventData, properties: { ...eventData.properties } }; isInstrumented = instrumentEventData(eventData, messageSpan); this._spanContexts.push(messageSpan.context()); messageSpan.end(); diff --git a/sdk/eventhub/event-hubs/src/eventHubClient.ts b/sdk/eventhub/event-hubs/src/eventHubClient.ts index 1a55678b5b0e..0bfce4c1b7ad 100644 --- a/sdk/eventhub/event-hubs/src/eventHubClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubClient.ts @@ -25,9 +25,9 @@ import { AbortSignalLike } from "@azure/abort-controller"; import { EventHubProducer } from "./sender"; import { EventHubConsumer } from "./receiver"; import { throwTypeErrorIfParameterMissing, throwErrorIfConnectionClosed } from "./util/error"; -import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from '@azure/core-tracing'; +import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from "@azure/core-tracing"; -type OperationNames = "getProperties"|"getPartitionIds"|"getPartitionProperties"; +type OperationNames = "getProperties" | "getPartitionIds" | "getPartitionProperties"; /** * @internal @@ -481,11 +481,11 @@ export class EventHubClient { kind: SpanKind.CLIENT, parent: parentSpan }); - + span.setAttribute("component", "eventhubs"); span.setAttribute("message_bus.destination", this.eventHubName); span.setAttribute("peer.address", this._endpoint); - + return span; } diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index fe7dd62e4f14..f8186f43f3fd 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -8,9 +8,9 @@ import { ConnectionContext } from "./connectionContext"; import * as log from "./log"; import { throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing } from "./util/error"; import { EventDataBatch } from "./eventDataBatch"; -import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from '@azure/core-tracing'; +import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from "@azure/core-tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; -import { createMessageSpan } from './diagnostics/messageSpan'; +import { createMessageSpan } from "./diagnostics/messageSpan"; /** * A producer responsible for sending events to an Event Hub. @@ -63,7 +63,12 @@ export class EventHubProducer { * @internal * @ignore */ - constructor(eventHubName: string, endpoint: string, context: ConnectionContext, options?: EventHubProducerOptions) { + constructor( + eventHubName: string, + endpoint: string, + context: ConnectionContext, + options?: EventHubProducerOptions + ) { this._context = context; this._senderOptions = options || {}; const partitionId = @@ -167,7 +172,7 @@ export class EventHubProducer { let spanContextsToLink: SpanContext[] = []; if (Array.isArray(eventData)) { for (let i = 0; i < eventData.length; i++) { - const event: EventData = {...eventData[i], properties: {...eventData[i].properties}}; + const event: EventData = { ...eventData[i], properties: { ...eventData[i].properties } }; const messageSpan = createMessageSpan(options.parentSpan); // since these message spans are created from same context as the send span, // these message spans don't need to be linked. @@ -178,15 +183,17 @@ export class EventHubProducer { spanContextsToLink = eventData._messageSpanContexts; } - const sendSpan = this._createSendSpan(options.parentSpan); for (const spanContext of spanContextsToLink) { sendSpan.addLink(spanContext); } try { - const result = await this._eventHubSender!.send(eventData, { ...this._senderOptions, ...options }); - sendSpan.setStatus({code: CanonicalCode.OK}); + const result = await this._eventHubSender!.send(eventData, { + ...this._senderOptions, + ...options + }); + sendSpan.setStatus({ code: CanonicalCode.OK }); return result; } catch (err) { sendSpan.setStatus({ From 04e0bc1362f5c8ebd98cfbb2768d495412fc773c Mon Sep 17 00:00:00 2001 From: chradek Date: Fri, 27 Sep 2019 15:08:02 -0700 Subject: [PATCH 5/9] adds tests and logging for core-tracing --- sdk/core/core-tracing/lib/index.ts | 6 +- .../opencensus/openCensusSpanWrapper.ts | 9 +- .../lib/utils/extractSpanContext.ts | 26 ---- .../core-tracing/lib/utils/getTraceParent.ts | 21 --- sdk/core/core-tracing/lib/utils/log.ts | 28 ++++ .../lib/utils/traceParentHeader.ts | 66 +++++++++ sdk/core/core-tracing/package.json | 2 + .../test/traceParentHeader.spec.ts | 128 ++++++++++++++++++ 8 files changed, 235 insertions(+), 51 deletions(-) delete mode 100644 sdk/core/core-tracing/lib/utils/extractSpanContext.ts delete mode 100644 sdk/core/core-tracing/lib/utils/getTraceParent.ts create mode 100644 sdk/core/core-tracing/lib/utils/log.ts create mode 100644 sdk/core/core-tracing/lib/utils/traceParentHeader.ts create mode 100644 sdk/core/core-tracing/test/traceParentHeader.spec.ts diff --git a/sdk/core/core-tracing/lib/index.ts b/sdk/core/core-tracing/lib/index.ts index 8165c45cccf2..ea545a226980 100644 --- a/sdk/core/core-tracing/lib/index.ts +++ b/sdk/core/core-tracing/lib/index.ts @@ -35,5 +35,7 @@ export { Tracer as OpenCensusTracer, Span as OpenCensusSpan, } from "@opencensus/web-types"; -export { getTraceParent } from "./utils/getTraceParent"; -export { extractSpanContextFromTraceParent } from "./utils/extractSpanContext"; +export { + extractSpanContextFromTraceParentHeader, + getTraceParentHeader +} from "./utils/traceParentHeader"; diff --git a/sdk/core/core-tracing/lib/tracers/opencensus/openCensusSpanWrapper.ts b/sdk/core/core-tracing/lib/tracers/opencensus/openCensusSpanWrapper.ts index af70c07d2866..e25c426ac059 100644 --- a/sdk/core/core-tracing/lib/tracers/opencensus/openCensusSpanWrapper.ts +++ b/sdk/core/core-tracing/lib/tracers/opencensus/openCensusSpanWrapper.ts @@ -7,7 +7,7 @@ import { Status } from "../../interfaces/status"; import { OpenCensusTraceStateWrapper } from "./openCensusTraceStateWrapper"; import { SpanOptions } from "../../interfaces/SpanOptions"; import { OpenCensusTracerWrapper } from "./openCensusTracerWrapper"; -import { Attributes as OpenCensusAttributes, Span as OpenCensusSpan, LinkType } from "@opencensus/web-types"; +import { Attributes as OpenCensusAttributes, Span as OpenCensusSpan } from "@opencensus/web-types"; function isWrappedSpan(span?: Span | SpanContext): span is OpenCensusSpanWrapper { return !!span && (span as OpenCensusSpanWrapper).getWrappedSpan !== undefined; @@ -101,7 +101,12 @@ export class OpenCensusSpanWrapper implements Span { addLink(spanContext: SpanContext, attributes?: Attributes): this { // Since there is no way to specify the link relationship, // it is set as Unspecified. - this._span.addLink(spanContext.traceId, spanContext.spanId, 0 /* LinkType.UNSPECIFIED */, attributes as OpenCensusAttributes); + this._span.addLink( + spanContext.traceId, + spanContext.spanId, + 0 /* LinkType.UNSPECIFIED */, + attributes as OpenCensusAttributes + ); return this; } diff --git a/sdk/core/core-tracing/lib/utils/extractSpanContext.ts b/sdk/core/core-tracing/lib/utils/extractSpanContext.ts deleted file mode 100644 index 5ca420373d0a..000000000000 --- a/sdk/core/core-tracing/lib/utils/extractSpanContext.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SpanContext } from '../interfaces/span_context'; - -/** - * 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 extractSpanContextFromTraceParent(traceParent: string): SpanContext { - const parts = traceParent.split("-"); - - if (parts.length !== 4) { - throw new Error(`Unable to extract span context from traceparent "${traceParent}".`); - } - - const [_, traceId, spanId, traceOptions] = parts; - - const traceFlags = parseInt(traceOptions, 16); - - const spanContext: SpanContext = { - spanId, - traceId, - traceFlags - }; - - return spanContext; -} \ No newline at end of file diff --git a/sdk/core/core-tracing/lib/utils/getTraceParent.ts b/sdk/core/core-tracing/lib/utils/getTraceParent.ts deleted file mode 100644 index 5cf04bf45cc0..000000000000 --- a/sdk/core/core-tracing/lib/utils/getTraceParent.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SpanContext } from "../interfaces/span_context"; -import { TraceFlags } from '../interfaces/trace_flags'; - -const VERSION = "00"; - -/** - * 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 getTraceParent(spanContext: SpanContext): string { - if (!spanContext.traceId || !spanContext.spanId) { - throw new Error(`Missing required fields "traceId" or "spanId" from spanContext.`); - } - - const flags = spanContext.traceFlags || TraceFlags.UNSAMPLED; - const traceFlags = (flags < 10) ? `0${flags.toString(16)}` : flags.toString(16); - - // https://www.w3.org/TR/trace-context/#traceparent-header-field-values - return `${VERSION}-${spanContext.traceId}-${spanContext.spanId}-${traceFlags}`; -} \ No newline at end of file diff --git a/sdk/core/core-tracing/lib/utils/log.ts b/sdk/core/core-tracing/lib/utils/log.ts new file mode 100644 index 000000000000..da8f10395e47 --- /dev/null +++ b/sdk/core/core-tracing/lib/utils/log.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import debug from "debug"; + +/** + * @ignore + * Log statements for errors that the application is unlikely to recover from. + */ +export const error = debug("azure:core-tracing:error"); + +/** + * @ignore + * Log statements for warnings when a function fails to perform its intended task. + */ +export const warning = debug("azure:core-tracing:warning"); + +/** + * @ignore + * Log statements for info when a function operates normally. + */ +export const info = debug("azure:core-tracing:info"); + +/** + * @ignore + * Log statements for verbose for troubleshooting scenarios. + */ +export const verbose = debug("azure:core-tracing:verbose"); diff --git a/sdk/core/core-tracing/lib/utils/traceParentHeader.ts b/sdk/core/core-tracing/lib/utils/traceParentHeader.ts new file mode 100644 index 000000000000..5abfddc82c25 --- /dev/null +++ b/sdk/core/core-tracing/lib/utils/traceParentHeader.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SpanContext } from '../interfaces/span_context'; +import { TraceFlags } from '../interfaces/trace_flags'; +import * as log from './log'; + +/** + * 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) { + log.warning(`Unable to extract span context from traceparent header "${traceParentHeader}".`); + return; + } + + const [version, traceId, spanId, traceOptions] = parts; + + if (version !== VERSION) { + log.warning(`Unexpected traceparent header version "${version}" found, expected "${VERSION}".`); + return; + } + + const traceFlags = parseInt(traceOptions, 16); + + const spanContext: SpanContext = { + spanId, + traceId, + traceFlags + }; + + return spanContext; +} + + +const VERSION = "00"; + +/** + * 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) { + log.warning(`Missing required field(s) ${missingFields.join(", ")} from spanContext`); + return; + } + + const flags = spanContext.traceFlags || TraceFlags.UNSAMPLED; + const traceFlags = (flags < 10) ? `0${flags.toString(16)}` : flags.toString(16); + + // 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/package.json b/sdk/core/core-tracing/package.json index 519bdb2d4eed..dbe985dbfd25 100644 --- a/sdk/core/core-tracing/package.json +++ b/sdk/core/core-tracing/package.json @@ -59,11 +59,13 @@ "sideEffects": false, "dependencies": { "@opencensus/web-types": "0.0.7", + "debug": "^4.1.1", "tslib": "^1.9.3" }, "devDependencies": { "@azure/eslint-plugin-azure-sdk": "^2.0.1", "@microsoft/api-extractor": "^7.1.5", + "@types/debug": "^4.1.4", "@types/mocha": "^5.2.5", "@types/node": "^8.0.0", "@typescript-eslint/eslint-plugin": "^2.0.0", diff --git a/sdk/core/core-tracing/test/traceParentHeader.spec.ts b/sdk/core/core-tracing/test/traceParentHeader.spec.ts new file mode 100644 index 000000000000..d6ca97410ee1 --- /dev/null +++ b/sdk/core/core-tracing/test/traceParentHeader.spec.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from "assert"; +import { + extractSpanContextFromTraceParentHeader, + TraceFlags, + SpanContext, + getTraceParentHeader +} from "../lib"; + +describe("traceParentHeader", () => { + describe("#extractSpanContextFromTraceParentHeader", () => { + it("should extract a SpanContext from a propertly 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.UNSAMPLED, + "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" + }; + + 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." + ); + }); + }); + }); +}); From ffe253572feab86cc7c8b83cdcfffe8c1695777e Mon Sep 17 00:00:00 2001 From: chradek Date: Fri, 27 Sep 2019 15:18:21 -0700 Subject: [PATCH 6/9] fix core-tracing build to work with tsts --- sdk/core/core-tracing/api-extractor.json | 51 +++++++++---------- sdk/core/core-tracing/lib/index.ts | 10 ++-- sdk/core/core-tracing/package.json | 4 +- .../core-tracing/review/core-tracing.api.md | 6 ++- sdk/core/core-tracing/rollup.base.config.js | 2 +- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/sdk/core/core-tracing/api-extractor.json b/sdk/core/core-tracing/api-extractor.json index ea431776e9eb..fc077a1e238e 100644 --- a/sdk/core/core-tracing/api-extractor.json +++ b/sdk/core/core-tracing/api-extractor.json @@ -1,32 +1,31 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "mainEntryPointFilePath": "types/index.d.ts", - "docModel": { - "enabled": false - }, - "apiReport": { - "enabled": true, - "reportFolder": "./review" - }, - "dtsRollup": { - "enabled": true, - "untrimmedFilePath": "", - "publicTrimmedFilePath": "./types/core-tracing.d.ts" + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/lib/index.d.ts", + "docModel": { + "enabled": false + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/core-tracing.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } }, - "messages": { - "tsdocMessageReporting": { - "default": { - "logLevel": "none" - } + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" }, - "extractorMessageReporting": { - "ae-missing-release-tag": { - "logLevel": "none" - }, - "ae-unresolved-link": { - "logLevel": "none" - } + "ae-unresolved-link": { + "logLevel": "none" } } } - \ No newline at end of file +} diff --git a/sdk/core/core-tracing/lib/index.ts b/sdk/core/core-tracing/lib/index.ts index ea545a226980..be56c188a823 100644 --- a/sdk/core/core-tracing/lib/index.ts +++ b/sdk/core/core-tracing/lib/index.ts @@ -30,12 +30,14 @@ export { TraceFlags } from "./interfaces/trace_flags"; export { TraceState } from "./interfaces/trace_state"; export { Tracer } from "./interfaces/tracer"; +// Utilities +export { + extractSpanContextFromTraceParentHeader, + getTraceParentHeader +} from "./utils/traceParentHeader"; + // OpenCensus Interfaces export { Tracer as OpenCensusTracer, Span as OpenCensusSpan, } from "@opencensus/web-types"; -export { - extractSpanContextFromTraceParentHeader, - getTraceParentHeader -} from "./utils/traceParentHeader"; diff --git a/sdk/core/core-tracing/package.json b/sdk/core/core-tracing/package.json index dbe985dbfd25..be37a0794f7a 100644 --- a/sdk/core/core-tracing/package.json +++ b/sdk/core/core-tracing/package.json @@ -4,7 +4,7 @@ "description": "Provides low-level interfaces and helper methods for tracing in Azure SDK", "sdk-type": "client", "main": "dist/index.js", - "module": "dist-esm/index.js", + "module": "dist-esm/lib/index.js", "browser": { "./dist/index.js": "./browser/index.js" }, @@ -37,7 +37,7 @@ "files": [ "browser/*.js*", "dist/", - "dist-esm/", + "dist-esm/lib/", "src/", "types/core-tracing.d.ts", "ThirdPartyNotices.txt" diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 7bdb1639fda5..716817888cf2 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -46,10 +46,12 @@ export interface Event { // @public export function getTracer(): Tracer; -export function extractSpanContextFromTraceParent(traceParent: string): SpanContext; +export function extractSpanContextFromTraceParentHeader( + traceParentHeader: string +): SpanContext | undefined; // @public -export function getTraceParent(spanContext: SpanContext): string; +export function getTraceParentHeader(spanContext: SpanContext): string | undefined; // @public export type HrTime = [number, number]; diff --git a/sdk/core/core-tracing/rollup.base.config.js b/sdk/core/core-tracing/rollup.base.config.js index e5af2fcc90c6..25f26d65265c 100644 --- a/sdk/core/core-tracing/rollup.base.config.js +++ b/sdk/core/core-tracing/rollup.base.config.js @@ -8,7 +8,7 @@ import viz from "rollup-plugin-visualizer"; const pkg = require("./package.json"); const depNames = Object.keys(pkg.dependencies); -const input = "dist-esm/index.js"; +const input = "dist-esm/lib/index.js"; const production = process.env.NODE_ENV === "production"; export function nodeConfig(test = false) { From 675455da81876ba272070288e1ffc9fe2e6c0810 Mon Sep 17 00:00:00 2001 From: chradek Date: Fri, 27 Sep 2019 16:45:07 -0700 Subject: [PATCH 7/9] updates based on feedback --- .../src/diagnostics/instrumentEventData.ts | 24 ++++++------ .../event-hubs/src/diagnostics/messageSpan.ts | 3 ++ sdk/eventhub/event-hubs/src/eventDataBatch.ts | 15 +++++--- sdk/eventhub/event-hubs/src/sender.ts | 6 ++- sdk/eventhub/event-hubs/test/sender.spec.ts | 37 +++++++++++++++++++ 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index af2ff47fbc19..a3225ea11ff2 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -1,21 +1,23 @@ -import { Span, getTraceParent } from "@azure/core-tracing"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Span, getTraceParentHeader } from "@azure/core-tracing"; import { EventData } from "../eventData"; export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; -export function instrumentEventData(eventData: EventData, span: Span): boolean { +export function instrumentEventData(eventData: EventData, span: Span): EventData { if (eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) { - return false; + return eventData; } - try { - const traceParent = getTraceParent(span.context()); - eventData.properties = eventData.properties || {}; - eventData.properties[TRACEPARENT_PROPERTY] = traceParent; - } catch { - // swallow the error, the event data won't be modified - return false; + // create a copy so the original isn't modified + eventData = { ...eventData, properties: { ...eventData.properties } }; + + const traceParent = getTraceParentHeader(span.context()); + if (traceParent) { + eventData.properties![TRACEPARENT_PROPERTY] = traceParent; } - return true; + return eventData; } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts index 16d4aa3f6ff6..ec60ad45a73a 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + import { SpanContext, Span, TracerProxy, SpanKind } from "@azure/core-tracing"; export function createMessageSpan(parentSpan?: Span | SpanContext): Span { diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 0ff69165b7df..1dcc14c570e2 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -143,13 +143,13 @@ export class EventDataBatch { public tryAdd(eventData: EventData, options: TryAddOptions = {}): boolean { throwTypeErrorIfParameterMissing(this._context.connectionId, "eventData", eventData); - let isInstrumented = false; // check if the event has already been instrumented - if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { + const previouslyInstrumented = Boolean( + eventData.properties && eventData.properties[TRACEPARENT_PROPERTY] + ); + if (!previouslyInstrumented) { const messageSpan = createMessageSpan(options.parentSpan); - // Create a shallow copy of eventData and eventData.properties in case we add the diagnostic id to the properties. - eventData = { ...eventData, properties: { ...eventData.properties } }; - isInstrumented = instrumentEventData(eventData, messageSpan); + eventData = instrumentEventData(eventData, messageSpan); this._spanContexts.push(messageSpan.context()); messageSpan.end(); } @@ -174,7 +174,10 @@ export class EventDataBatch { // this._batchMessage will be used for final send operation if (currentSize > this._maxSizeInBytes) { this._encodedMessages.pop(); - if (isInstrumented) { + if ( + !previouslyInstrumented && + Boolean(eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) + ) { this._spanContexts.pop(); } return false; diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index f8186f43f3fd..9f014d203709 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -172,11 +172,12 @@ export class EventHubProducer { let spanContextsToLink: SpanContext[] = []; if (Array.isArray(eventData)) { for (let i = 0; i < eventData.length; i++) { - const event: EventData = { ...eventData[i], properties: { ...eventData[i].properties } }; const messageSpan = createMessageSpan(options.parentSpan); // since these message spans are created from same context as the send span, // these message spans don't need to be linked. - instrumentEventData(event, messageSpan); + const event = instrumentEventData(eventData[i], messageSpan); + // replace the original event with the instrumented one + eventData[i] = event; messageSpan.end(); } } else if (eventData instanceof EventDataBatch) { @@ -200,6 +201,7 @@ export class EventHubProducer { code: CanonicalCode.UNKNOWN, message: err.message }); + throw err; } finally { sendSpan.end(); } diff --git a/sdk/eventhub/event-hubs/test/sender.spec.ts b/sdk/eventhub/event-hubs/test/sender.spec.ts index 382970365a1c..0e92be494215 100644 --- a/sdk/eventhub/event-hubs/test/sender.spec.ts +++ b/sdk/eventhub/event-hubs/test/sender.spec.ts @@ -10,6 +10,7 @@ const debug = debugModule("azure:event-hubs:sender-spec"); import { EventHubClient, EventData, EventHubProducer, EventPosition } from "../src"; import { EnvVarKeys, getEnvVars } from "./utils/testUtils"; import { AbortController } from "@azure/abort-controller"; +//import { SpanContext } from "@azure/core-tracing"; const env = getEnvVars(); describe("EventHub Sender #RunnableInBrowser", function(): void { @@ -225,6 +226,42 @@ describe("EventHub Sender #RunnableInBrowser", function(): void { await consumer.close(); }); + /** + * This test can be uncommented once a test tracer exists. + * Currently + */ + // it("should support instrumentation", async function(): Promise { + // const list = [{ name: "Albert" }, { name: "Marie" }]; + // const partitionInfo = await client.getPartitionProperties("0"); + // const producer = client.createProducer({ partitionId: "0" }); + // const consumer = client.createConsumer( + // EventHubClient.defaultConsumerGroupName, + // "0", + // EventPosition.fromSequenceNumber(partitionInfo.lastEnqueuedSequenceNumber) + // ); + // const eventDataBatch = await producer.createBatch(); + // for (let i = 0; i < 2; i++) { + // eventDataBatch.tryAdd( + // { body: `${list[i].name}` }, + // { + // parentSpan: { + // traceId: "11111111111111111111111111111111", + // spanId: "2222222222222222" + // } as SpanContext + // } + // ); + // } + // await producer.send(eventDataBatch); + // const data = await consumer.receiveBatch(3, 5); + // data.length.should.equal(2); + // list[0].name.should.equal(data[0].body); + // data[0].properties!["Diagnostic-Id"].should.exist; + // list[1].name.should.equal(data[1].body); + // data[1].properties!["Diagnostic-Id"].should.exist; + // await producer.close(); + // await consumer.close(); + // }); + it("with partition key should be sent successfully.", async function(): Promise { const producer = client.createProducer(); const eventDataBatch = await producer.createBatch({ partitionKey: "1" }); From f1bd81d2efebfa0ff2bbed47dbebdd496aabb374 Mon Sep 17 00:00:00 2001 From: chradek Date: Mon, 30 Sep 2019 11:07:43 -0700 Subject: [PATCH 8/9] adds method to extract SpanContext from EventData --- .../lib/utils/traceParentHeader.ts | 20 ++--- .../test/traceParentHeader.spec.ts | 2 +- .../event-hubs/review/event-hubs.api.md | 3 + .../src/diagnostics/instrumentEventData.ts | 31 +++++++- sdk/eventhub/event-hubs/src/eventDataBatch.ts | 15 ++++ sdk/eventhub/event-hubs/src/index.ts | 1 + sdk/eventhub/event-hubs/src/sender.ts | 4 +- sdk/eventhub/event-hubs/test/misc.spec.ts | 77 ++++++++++++++++++- 8 files changed, 139 insertions(+), 14 deletions(-) diff --git a/sdk/core/core-tracing/lib/utils/traceParentHeader.ts b/sdk/core/core-tracing/lib/utils/traceParentHeader.ts index 5abfddc82c25..8968a739687a 100644 --- a/sdk/core/core-tracing/lib/utils/traceParentHeader.ts +++ b/sdk/core/core-tracing/lib/utils/traceParentHeader.ts @@ -1,16 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { SpanContext } from '../interfaces/span_context'; -import { TraceFlags } from '../interfaces/trace_flags'; -import * as log from './log'; +import { SpanContext } from "../interfaces/span_context"; +import { TraceFlags } from "../interfaces/trace_flags"; +import * as log from "./log"; + +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 { +export function extractSpanContextFromTraceParentHeader( + traceParentHeader: string +): SpanContext | undefined { const parts = traceParentHeader.split("-"); if (parts.length !== 4) { @@ -36,11 +40,8 @@ export function extractSpanContextFromTraceParentHeader(traceParentHeader: strin return spanContext; } - -const VERSION = "00"; - /** - * Generates a `traceparent` value given a span context. + * Generates a `traceparent` value given a span context. * @param spanContext Contains context for a specific span. * @returns The `spanContext` represented as a `traceparent` value. */ @@ -59,7 +60,8 @@ export function getTraceParentHeader(spanContext: SpanContext): string | undefin } const flags = spanContext.traceFlags || TraceFlags.UNSAMPLED; - const traceFlags = (flags < 10) ? `0${flags.toString(16)}` : flags.toString(16); + 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/traceParentHeader.spec.ts b/sdk/core/core-tracing/test/traceParentHeader.spec.ts index d6ca97410ee1..c36d6dae0fd9 100644 --- a/sdk/core/core-tracing/test/traceParentHeader.spec.ts +++ b/sdk/core/core-tracing/test/traceParentHeader.spec.ts @@ -11,7 +11,7 @@ import { describe("traceParentHeader", () => { describe("#extractSpanContextFromTraceParentHeader", () => { - it("should extract a SpanContext from a propertly formatted traceparent", () => { + it("should extract a SpanContext from a properly formatted traceparent", () => { const traceId = "11111111111111111111111111111111"; const spanId = "2222222222222222"; const flags = "00"; diff --git a/sdk/eventhub/event-hubs/review/event-hubs.api.md b/sdk/eventhub/event-hubs/review/event-hubs.api.md index c3540b73029d..22f05590439c 100644 --- a/sdk/eventhub/event-hubs/review/event-hubs.api.md +++ b/sdk/eventhub/event-hubs/review/event-hubs.api.md @@ -192,6 +192,9 @@ export interface EventProcessorOptions { trackLastEnqueuedEventInfo?: boolean; } +// @public +export function extractSpanContextFromEventData(eventData: EventData): SpanContext | undefined; + // @public export interface GetPartitionIdsOptions extends AbortSignalOptions, ParentSpanOptions { } diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index a3225ea11ff2..aa4a6032c266 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -1,11 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Span, getTraceParentHeader } from "@azure/core-tracing"; +import { + Span, + getTraceParentHeader, + SpanContext, + extractSpanContextFromTraceParentHeader +} from "@azure/core-tracing"; import { EventData } from "../eventData"; +/** + * @ignore + */ export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; +/** + * Populates the `EventData` with `SpanContext` info to support trace propagation. + * Creates and returns a copy of the passed in `EventData` unless the `EventData` + * has already been instrumented. + * @param eventData The `EventData` to instrument. + * @param span The `Span` containing the context to propagate tracing information. + * @ignore + */ export function instrumentEventData(eventData: EventData, span: Span): EventData { if (eventData.properties && eventData.properties[TRACEPARENT_PROPERTY]) { return eventData; @@ -21,3 +37,16 @@ export function instrumentEventData(eventData: EventData, span: Span): EventData return eventData; } + +/** + * Extracts the `SpanContext` from an `EventData` if the context exists. + * @param eventData An individual `EventData` object. + */ +export function extractSpanContextFromEventData(eventData: EventData): SpanContext | undefined { + if (!eventData.properties || !eventData.properties[TRACEPARENT_PROPERTY]) { + return; + } + + const diagnosticId = eventData.properties[TRACEPARENT_PROPERTY]; + return extractSpanContextFromTraceParentHeader(diagnosticId); +} diff --git a/sdk/eventhub/event-hubs/src/eventDataBatch.ts b/sdk/eventhub/event-hubs/src/eventDataBatch.ts index 1dcc14c570e2..1b75aa6e5ced 100644 --- a/sdk/eventhub/event-hubs/src/eventDataBatch.ts +++ b/sdk/eventhub/event-hubs/src/eventDataBatch.ts @@ -10,6 +10,21 @@ import { Span, SpanContext } from "@azure/core-tracing"; import { instrumentEventData, TRACEPARENT_PROPERTY } from "./diagnostics/instrumentEventData"; import { createMessageSpan } from "./diagnostics/messageSpan"; +/** + * Checks if the provided eventDataBatch is an instance of `EventDataBatch`. + * @param eventDataBatch The instance of `EventDataBatch` to verify. + * @internal + * @ignore + */ +export function isEventDataBatch(eventDataBatch: any): eventDataBatch is EventDataBatch { + return ( + eventDataBatch && + typeof eventDataBatch.tryAdd === "function" && + typeof eventDataBatch.count === "number" && + typeof eventDataBatch.sizeInBytes === "number" + ); +} + /** * The set of options to configure the behavior of `tryAdd`. * - `parentSpan` : The `Span` or `SpanContext` to use as the `parent` of the span created while calling this operation. diff --git a/sdk/eventhub/event-hubs/src/index.ts b/sdk/eventhub/event-hubs/src/index.ts index 2eeab1c1d60a..ee8a7f860be5 100644 --- a/sdk/eventhub/event-hubs/src/index.ts +++ b/sdk/eventhub/event-hubs/src/index.ts @@ -34,6 +34,7 @@ export { } from "./eventProcessor"; export { InMemoryPartitionManager } from "./inMemoryPartitionManager"; export { PartitionProcessor, Checkpoint } from "./partitionProcessor"; +export { extractSpanContextFromEventData } from "./diagnostics/instrumentEventData"; export { MessagingError, DataTransformer, diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index 9f014d203709..6a325661772e 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -7,7 +7,7 @@ import { EventHubProducerOptions, SendOptions, BatchOptions } from "./eventHubCl import { ConnectionContext } from "./connectionContext"; import * as log from "./log"; import { throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing } from "./util/error"; -import { EventDataBatch } from "./eventDataBatch"; +import { EventDataBatch, isEventDataBatch } from "./eventDataBatch"; import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from "@azure/core-tracing"; import { instrumentEventData } from "./diagnostics/instrumentEventData"; import { createMessageSpan } from "./diagnostics/messageSpan"; @@ -180,7 +180,7 @@ export class EventHubProducer { eventData[i] = event; messageSpan.end(); } - } else if (eventData instanceof EventDataBatch) { + } else if (isEventDataBatch(eventData)) { spanContextsToLink = eventData._messageSpanContexts; } diff --git a/sdk/eventhub/event-hubs/test/misc.spec.ts b/sdk/eventhub/event-hubs/test/misc.spec.ts index ffcd47a60c78..67ad9109f123 100644 --- a/sdk/eventhub/event-hubs/test/misc.spec.ts +++ b/sdk/eventhub/event-hubs/test/misc.spec.ts @@ -14,9 +14,15 @@ import { EventHubClient, EventData, EventHubProperties, - EventHubConsumer + EventHubConsumer, + ReceivedEventData } from "../src"; import { EnvVarKeys, getEnvVars } from "./utils/testUtils"; +import { + TRACEPARENT_PROPERTY, + extractSpanContextFromEventData +} from "../src/diagnostics/instrumentEventData"; +import { TraceFlags } from "@azure/core-tracing"; const env = getEnvVars(); describe("Misc tests #RunnableInBrowser", function(): void { @@ -317,4 +323,73 @@ describe("Misc tests #RunnableInBrowser", function(): void { } should.equal(totalReceived, msgToSendCount); }); + + 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}` + } + }; + + 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.UNSAMPLED, + "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}` + } + }; + + 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 + }; + + const spanContext = extractSpanContextFromEventData(eventData); + + should.not.exist( + spanContext, + `Missing property "${TRACEPARENT_PROPERTY}" should return undefined spanContext.` + ); + }); + }); }).timeout(60000); From 789dab6136cbb2f52102a20eeca73a1cbbcce52b Mon Sep 17 00:00:00 2001 From: chradek Date: Mon, 30 Sep 2019 21:08:31 -0700 Subject: [PATCH 9/9] adds tests for event hubs tracing --- .../core-tracing/review/core-tracing.api.md | 276 +++++++------- .../src/diagnostics/instrumentEventData.ts | 2 +- .../event-hubs/src/diagnostics/messageSpan.ts | 4 +- sdk/eventhub/event-hubs/src/eventHubClient.ts | 4 +- sdk/eventhub/event-hubs/src/sender.ts | 22 +- .../event-hubs/test/hubruntime.spec.ts | 110 ++++++ sdk/eventhub/event-hubs/test/sender.spec.ts | 339 ++++++++++++++++-- 7 files changed, 566 insertions(+), 191 deletions(-) diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 716817888cf2..2d374a2c1ed7 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -3,131 +3,132 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { Span as OpenCensusSpan } from "@opencensus/web-types"; -import { Tracer as OpenCensusTracer } from "@opencensus/web-types"; + +import { Span as OpenCensusSpan } from '@opencensus/web-types'; +import { Tracer as OpenCensusTracer } from '@opencensus/web-types'; // @public export interface Attributes { - [attributeKey: string]: unknown; + [attributeKey: string]: unknown; } // @public export interface BinaryFormat { - fromBytes(buffer: ArrayBuffer): SpanContext | null; - toBytes(spanContext: SpanContext): ArrayBuffer; + fromBytes(buffer: ArrayBuffer): SpanContext | null; + toBytes(spanContext: SpanContext): ArrayBuffer; } // @public export enum CanonicalCode { - ABORTED = 10, - ALREADY_EXISTS = 6, - CANCELLED = 1, - DATA_LOSS = 15, - DEADLINE_EXCEEDED = 4, - FAILED_PRECONDITION = 9, - INTERNAL = 13, - INVALID_ARGUMENT = 3, - NOT_FOUND = 5, - OK = 0, - OUT_OF_RANGE = 11, - PERMISSION_DENIED = 7, - RESOURCE_EXHAUSTED = 8, - UNAUTHENTICATED = 16, - UNAVAILABLE = 14, - UNIMPLEMENTED = 12, - UNKNOWN = 2 + ABORTED = 10, + ALREADY_EXISTS = 6, + CANCELLED = 1, + DATA_LOSS = 15, + DEADLINE_EXCEEDED = 4, + FAILED_PRECONDITION = 9, + INTERNAL = 13, + INVALID_ARGUMENT = 3, + NOT_FOUND = 5, + OK = 0, + OUT_OF_RANGE = 11, + PERMISSION_DENIED = 7, + RESOURCE_EXHAUSTED = 8, + UNAUTHENTICATED = 16, + UNAVAILABLE = 14, + UNIMPLEMENTED = 12, + UNKNOWN = 2 } // @public export interface Event { - attributes?: Attributes; - name: string; + attributes?: Attributes; + name: string; } // @public -export function getTracer(): Tracer; -export function extractSpanContextFromTraceParentHeader( - traceParentHeader: string -): SpanContext | undefined; +export function extractSpanContextFromTraceParentHeader(traceParentHeader: string): SpanContext | undefined; // @public export function getTraceParentHeader(spanContext: SpanContext): string | undefined; +// @public +export function getTracer(): Tracer; + // @public export type HrTime = [number, number]; // @public export interface HttpTextFormat { - extract(format: string, carrier: unknown): SpanContext | null; - inject(spanContext: SpanContext, format: string, carrier: unknown): void; + extract(format: string, carrier: unknown): SpanContext | null; + inject(spanContext: SpanContext, format: string, carrier: unknown): void; } // @public export interface Link { - attributes?: Attributes; - spanContext: SpanContext; + attributes?: Attributes; + spanContext: SpanContext; } // @public export class NoOpSpan implements Span { - addEvent(_name: string, _attributes?: Attributes): this; - addLink(_spanContext: SpanContext, _attributes?: Attributes): this; - context(): SpanContext; - end(_endTime?: number): void; - isRecordingEvents(): boolean; - setAttribute(_key: string, _value: unknown): this; - setAttributes(_attributes: Attributes): this; - setStatus(_status: Status): this; - updateName(_name: string): this; + addEvent(_name: string, _attributes?: Attributes): this; + addLink(_spanContext: SpanContext, _attributes?: Attributes): this; + context(): SpanContext; + end(_endTime?: number): void; + isRecordingEvents(): boolean; + setAttribute(_key: string, _value: unknown): this; + setAttributes(_attributes: Attributes): this; + setStatus(_status: Status): this; + updateName(_name: string): this; } // @public export class NoOpTracer implements Tracer { - bind(target: T, _span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span; - getHttpTextFormat(): HttpTextFormat; - recordSpanData(_span: Span): void; - startSpan(_name: string, _options?: SpanOptions): Span; - withSpan ReturnType>(_span: Span, fn: T): ReturnType; + bind(target: T, _span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span; + getHttpTextFormat(): HttpTextFormat; + recordSpanData(_span: Span): void; + startSpan(_name: string, _options?: SpanOptions): Span; + withSpan ReturnType>(_span: Span, fn: T): ReturnType; } -export { OpenCensusSpan }; +export { OpenCensusSpan } // @public export class OpenCensusSpanWrapper implements Span { - constructor(tracer: OpenCensusTracerWrapper, name: string, options?: SpanOptions); - addEvent(name: string, attributes?: Attributes): this; - addLink(spanContext: SpanContext, attributes?: Attributes): this; - context(): SpanContext; - end(_endTime?: number): void; - getWrappedSpan(): OpenCensusSpan; - isRecordingEvents(): boolean; - setAttribute(key: string, value: unknown): this; - setAttributes(attributes: Attributes): this; - setStatus(status: Status): this; - updateName(name: string): this; + constructor(tracer: OpenCensusTracerWrapper, name: string, options?: SpanOptions); + addEvent(name: string, attributes?: Attributes): this; + addLink(spanContext: SpanContext, attributes?: Attributes): this; + context(): SpanContext; + end(_endTime?: number): void; + getWrappedSpan(): OpenCensusSpan; + isRecordingEvents(): boolean; + setAttribute(key: string, value: unknown): this; + setAttributes(attributes: Attributes): this; + setStatus(status: Status): this; + updateName(name: string): this; } -export { OpenCensusTracer }; +export { OpenCensusTracer } // @public export class OpenCensusTracerWrapper implements Tracer { - constructor(tracer: OpenCensusTracer); - bind(target: T, span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span | null; - getHttpTextFormat(): HttpTextFormat; - getWrappedTracer(): OpenCensusTracer; - recordSpanData(span: Span): void; - startSpan(name: string, options?: SpanOptions): Span; - withSpan unknown>(span: Span, fn: T): ReturnType; + constructor(tracer: OpenCensusTracer); + bind(target: T, span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span | null; + getHttpTextFormat(): HttpTextFormat; + getWrappedTracer(): OpenCensusTracer; + recordSpanData(span: Span): void; + startSpan(name: string, options?: SpanOptions): Span; + withSpan unknown>(span: Span, fn: T): ReturnType; } // @public export interface Sampler { - shouldSample(parentContext?: SpanContext): boolean; - toString(): string; + shouldSample(parentContext?: SpanContext): boolean; + toString(): string; } // @public @@ -135,95 +136,88 @@ export function setTracer(tracer: Tracer): void; // @public export interface Span { - addEvent(name: string, attributes?: Attributes): this; - addLink(spanContext: SpanContext, attributes?: Attributes): this; - context(): SpanContext; - end(endTime?: TimeInput): void; - isRecordingEvents(): boolean; - setAttribute(key: string, value: unknown): this; - setAttributes(attributes: Attributes): this; - setStatus(status: Status): this; - updateName(name: string): this; + addEvent(name: string, attributes?: Attributes): this; + addLink(spanContext: SpanContext, attributes?: Attributes): this; + context(): SpanContext; + end(endTime?: TimeInput): void; + isRecordingEvents(): boolean; + setAttribute(key: string, value: unknown): this; + setAttributes(attributes: Attributes): this; + setStatus(status: Status): this; + updateName(name: string): this; } // @public export interface SpanContext { - spanId: string; - traceFlags?: TraceFlags; - traceId: string; - traceState?: TraceState; + spanId: string; + traceFlags?: TraceFlags; + traceId: string; + traceState?: TraceState; } // @public export interface SpanGraph { - roots: SpanGraphNode[]; + roots: SpanGraphNode[]; } // @public export interface SpanGraphNode { - children: SpanGraphNode[]; - name: string; + children: SpanGraphNode[]; + name: string; } // @public export enum SpanKind { - CLIENT = 2, - CONSUMER = 4, - INTERNAL = 0, - PRODUCER = 3, - SERVER = 1 + CLIENT = 2, + CONSUMER = 4, + INTERNAL = 0, + PRODUCER = 3, + SERVER = 1 } // @public export interface SpanOptions { - attributes?: Attributes; - isRecordingEvents?: boolean; - kind?: SpanKind; - parent?: Span | SpanContext; - startTime?: number; + attributes?: Attributes; + isRecordingEvents?: boolean; + kind?: SpanKind; + parent?: Span | SpanContext; + startTime?: number; } // @public export interface Status { - code: CanonicalCode; - message?: string; + code: CanonicalCode; + message?: string; } // @public export class TestSpan extends NoOpSpan { - constructor( - parentTracer: TestTracer, - name: string, - context: SpanContext, - kind: SpanKind, - parentSpanId?: string, - startTime?: TimeInput - ); - context(): SpanContext; - end(_endTime?: number): void; - endCalled: boolean; - isRecordingEvents(): boolean; - kind: SpanKind; - name: string; - readonly parentSpanId?: string; - setStatus(status: Status): this; - readonly startTime: TimeInput; - status: Status; - tracer(): Tracer; -} + constructor(parentTracer: TestTracer, name: string, context: SpanContext, kind: SpanKind, parentSpanId?: string, startTime?: TimeInput); + context(): SpanContext; + end(_endTime?: number): void; + endCalled: boolean; + isRecordingEvents(): boolean; + kind: SpanKind; + name: string; + readonly parentSpanId?: string; + setStatus(status: Status): this; + readonly startTime: TimeInput; + status: Status; + tracer(): Tracer; + } // @public export class TestTracer extends NoOpTracer { - getActiveSpans(): TestSpan[]; - getKnownSpans(): TestSpan[]; - getRootSpans(): TestSpan[]; - getSpanGraph(traceId: string): SpanGraph; - startSpan(name: string, options?: SpanOptions): TestSpan; -} + getActiveSpans(): TestSpan[]; + getKnownSpans(): TestSpan[]; + getRootSpans(): TestSpan[]; + getSpanGraph(traceId: string): SpanGraph; + startSpan(name: string, options?: SpanOptions): TestSpan; + } // @public export interface TimedEvent extends Event { - time: HrTime; + time: HrTime; } // @public @@ -231,28 +225,30 @@ export type TimeInput = HrTime | number | Date; // @public export enum TraceFlags { - SAMPLED = 1, - UNSAMPLED = 0 + SAMPLED = 1, + UNSAMPLED = 0 } // @public export interface Tracer { - bind(target: T, span?: Span): T; - getBinaryFormat(): BinaryFormat; - getCurrentSpan(): Span | null; - getHttpTextFormat(): HttpTextFormat; - recordSpanData(span: Span): void; - startSpan(name: string, options?: SpanOptions): Span; - withSpan ReturnType>(span: Span, fn: T): ReturnType; + bind(target: T, span?: Span): T; + getBinaryFormat(): BinaryFormat; + getCurrentSpan(): Span | null; + getHttpTextFormat(): HttpTextFormat; + recordSpanData(span: Span): void; + startSpan(name: string, options?: SpanOptions): Span; + withSpan ReturnType>(span: Span, fn: T): ReturnType; } // @public export interface TraceState { - get(key: string): string | undefined; - serialize(): string; - set(key: string, value: string): void; - unset(key: string): void; + get(key: string): string | undefined; + serialize(): string; + set(key: string, value: string): void; + unset(key: string): void; } + // (No @packageDocumentation comment for this package) + ``` diff --git a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts index aa4a6032c266..6cc650893a50 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/instrumentEventData.ts @@ -12,7 +12,7 @@ import { EventData } from "../eventData"; /** * @ignore */ -export const TRACEPARENT_PROPERTY = "Diagnostic_Id"; +export const TRACEPARENT_PROPERTY = "Diagnostic-Id"; /** * Populates the `EventData` with `SpanContext` info to support trace propagation. diff --git a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts index ec60ad45a73a..8658e4259979 100644 --- a/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts +++ b/sdk/eventhub/event-hubs/src/diagnostics/messageSpan.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { SpanContext, Span, TracerProxy, SpanKind } from "@azure/core-tracing"; +import { SpanContext, Span, getTracer, SpanKind } from "@azure/core-tracing"; export function createMessageSpan(parentSpan?: Span | SpanContext): Span { - const tracer = TracerProxy.getTracer(); + const tracer = getTracer(); const span = tracer.startSpan("Azure.EventHubs.message", { kind: SpanKind.INTERNAL, parent: parentSpan diff --git a/sdk/eventhub/event-hubs/src/eventHubClient.ts b/sdk/eventhub/event-hubs/src/eventHubClient.ts index 0bfce4c1b7ad..c0cb876821b5 100644 --- a/sdk/eventhub/event-hubs/src/eventHubClient.ts +++ b/sdk/eventhub/event-hubs/src/eventHubClient.ts @@ -25,7 +25,7 @@ import { AbortSignalLike } from "@azure/abort-controller"; import { EventHubProducer } from "./sender"; import { EventHubConsumer } from "./receiver"; import { throwTypeErrorIfParameterMissing, throwErrorIfConnectionClosed } from "./util/error"; -import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from "@azure/core-tracing"; +import { SpanContext, Span, getTracer, SpanKind, CanonicalCode } from "@azure/core-tracing"; type OperationNames = "getProperties" | "getPartitionIds" | "getPartitionProperties"; @@ -476,7 +476,7 @@ export class EventHubClient { } private _createClientSpan(operationName: OperationNames, parentSpan?: Span | SpanContext): Span { - const tracer = TracerProxy.getTracer(); + const tracer = getTracer(); const span = tracer.startSpan(`Azure.EventHubs.${operationName}`, { kind: SpanKind.CLIENT, parent: parentSpan diff --git a/sdk/eventhub/event-hubs/src/sender.ts b/sdk/eventhub/event-hubs/src/sender.ts index 6a325661772e..156c1d262aad 100644 --- a/sdk/eventhub/event-hubs/src/sender.ts +++ b/sdk/eventhub/event-hubs/src/sender.ts @@ -8,8 +8,8 @@ import { ConnectionContext } from "./connectionContext"; import * as log from "./log"; import { throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing } from "./util/error"; import { EventDataBatch, isEventDataBatch } from "./eventDataBatch"; -import { SpanContext, Span, TracerProxy, SpanKind, CanonicalCode } from "@azure/core-tracing"; -import { instrumentEventData } from "./diagnostics/instrumentEventData"; +import { SpanContext, Span, getTracer, SpanKind, CanonicalCode } from "@azure/core-tracing"; +import { instrumentEventData, TRACEPARENT_PROPERTY } from "./diagnostics/instrumentEventData"; import { createMessageSpan } from "./diagnostics/messageSpan"; /** @@ -172,13 +172,15 @@ export class EventHubProducer { let spanContextsToLink: SpanContext[] = []; if (Array.isArray(eventData)) { for (let i = 0; i < eventData.length; i++) { - const messageSpan = createMessageSpan(options.parentSpan); - // since these message spans are created from same context as the send span, - // these message spans don't need to be linked. - const event = instrumentEventData(eventData[i], messageSpan); - // replace the original event with the instrumented one - eventData[i] = event; - messageSpan.end(); + const event = eventData[i]; + if (!event.properties || !event.properties[TRACEPARENT_PROPERTY]) { + const messageSpan = createMessageSpan(options.parentSpan); + // since these message spans are created from same context as the send span, + // these message spans don't need to be linked. + // replace the original event with the instrumented one + eventData[i] = instrumentEventData(eventData[i], messageSpan); + messageSpan.end(); + } } } else if (isEventDataBatch(eventData)) { spanContextsToLink = eventData._messageSpanContexts; @@ -234,7 +236,7 @@ export class EventHubProducer { } private _createSendSpan(parentSpan?: Span | SpanContext): Span { - const tracer = TracerProxy.getTracer(); + const tracer = getTracer(); const span = tracer.startSpan("Azure.EventHubs.send", { kind: SpanKind.PRODUCER, parent: parentSpan diff --git a/sdk/eventhub/event-hubs/test/hubruntime.spec.ts b/sdk/eventhub/event-hubs/test/hubruntime.spec.ts index 7464a7a80e15..0c44466f25c5 100644 --- a/sdk/eventhub/event-hubs/test/hubruntime.spec.ts +++ b/sdk/eventhub/event-hubs/test/hubruntime.spec.ts @@ -12,6 +12,7 @@ const env = getEnvVars(); import { EventHubClient } from "../src"; import { AbortController } from "@azure/abort-controller"; +import { TestTracer, setTracer, SpanGraph } from "@azure/core-tracing"; describe("RuntimeInformation #RunnableInBrowser", function(): void { let client: EventHubClient; const service = { @@ -74,6 +75,43 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { }); }); + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + client = new EventHubClient(service.connectionString, service.path); + const ids = await client.getPartitionIds({ parentSpan: 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.getPartitionIds", + children: [ + { + name: "Azure.EventHubs.getProperties", + children: [] + } + ] + } + ] + } + ] + }; + + tracer.getSpanGraph(rootSpan.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + }); + describe("hub runtime information", function(): void { it("gets the hub runtime information", async function(): Promise { client = new EventHubClient(service.connectionString, service.path, { @@ -104,6 +142,40 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { err.message.should.match(/The [\w]+ operation has been cancelled by the user.$/gi); } }); + + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + client = new EventHubClient(service.connectionString, service.path); + const hubRuntimeInfo = await client.getProperties({ parentSpan: 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.getProperties", + children: [] + } + ] + } + ] + }; + + tracer.getSpanGraph(rootSpan.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + }); }); describe("partition runtime information", function(): void { @@ -174,5 +246,43 @@ describe("RuntimeInformation #RunnableInBrowser", function(): void { err.message.should.match(/The [\w]+ operation has been cancelled by the user.$/gi); } }); + + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + client = new EventHubClient(service.connectionString, service.path); + const partitionRuntimeInfo = await client.getPartitionProperties("0", { + parentSpan: rootSpan + }); + partitionRuntimeInfo.partitionId.should.equal("0"); + partitionRuntimeInfo.eventHubName.should.equal(service.path); + partitionRuntimeInfo.lastEnqueuedTimeUtc.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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + }); }); }).timeout(60000); diff --git a/sdk/eventhub/event-hubs/test/sender.spec.ts b/sdk/eventhub/event-hubs/test/sender.spec.ts index 0e92be494215..84f1f5fd61df 100644 --- a/sdk/eventhub/event-hubs/test/sender.spec.ts +++ b/sdk/eventhub/event-hubs/test/sender.spec.ts @@ -10,7 +10,8 @@ const debug = debugModule("azure:event-hubs:sender-spec"); import { EventHubClient, EventData, EventHubProducer, EventPosition } from "../src"; import { EnvVarKeys, getEnvVars } from "./utils/testUtils"; import { AbortController } from "@azure/abort-controller"; -//import { SpanContext } from "@azure/core-tracing"; +import { TestTracer, setTracer, SpanGraph } from "@azure/core-tracing"; +import { TRACEPARENT_PROPERTY } from "../src/diagnostics/instrumentEventData"; const env = getEnvVars(); describe("EventHub Sender #RunnableInBrowser", function(): void { @@ -103,6 +104,48 @@ describe("EventHub Sender #RunnableInBrowser", function(): void { ); } }); + + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + + const producer = client.createProducer({ partitionId: "0" }); + + await producer.send( + { body: "single message - manual trace propagation" }, + { parentSpan: 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.send", + children: [] + } + ] + } + ] + }; + + tracer.getSpanGraph(rootSpan.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); }); describe("Batch message", function(): void { @@ -226,41 +269,150 @@ describe("EventHub Sender #RunnableInBrowser", function(): void { await consumer.close(); }); - /** - * This test can be uncommented once a test tracer exists. - * Currently - */ - // it("should support instrumentation", async function(): Promise { - // const list = [{ name: "Albert" }, { name: "Marie" }]; - // const partitionInfo = await client.getPartitionProperties("0"); - // const producer = client.createProducer({ partitionId: "0" }); - // const consumer = client.createConsumer( - // EventHubClient.defaultConsumerGroupName, - // "0", - // EventPosition.fromSequenceNumber(partitionInfo.lastEnqueuedSequenceNumber) - // ); - // const eventDataBatch = await producer.createBatch(); - // for (let i = 0; i < 2; i++) { - // eventDataBatch.tryAdd( - // { body: `${list[i].name}` }, - // { - // parentSpan: { - // traceId: "11111111111111111111111111111111", - // spanId: "2222222222222222" - // } as SpanContext - // } - // ); - // } - // await producer.send(eventDataBatch); - // const data = await consumer.receiveBatch(3, 5); - // data.length.should.equal(2); - // list[0].name.should.equal(data[0].body); - // data[0].properties!["Diagnostic-Id"].should.exist; - // list[1].name.should.equal(data[1].body); - // data[1].properties!["Diagnostic-Id"].should.exist; - // await producer.close(); - // await consumer.close(); - // }); + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + + const list = [{ name: "Albert" }, { name: "Marie" }]; + const producer = client.createProducer({ partitionId: "0" }); + + const eventDataBatch = await producer.createBatch(); + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd({ body: `${list[i].name}` }, { parentSpan: rootSpan }); + } + await producer.send(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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); + + it("will not instrument already instrumented events", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("test"); + + const list = [ + { name: "Albert" }, + { + name: "Marie", + properties: { + [TRACEPARENT_PROPERTY]: "foo" + } + } + ]; + + const producer = client.createProducer({ partitionId: "0" }); + + const eventDataBatch = await producer.createBatch(); + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd( + { body: `${list[i].name}`, properties: list[i].properties }, + { parentSpan: rootSpan } + ); + } + await producer.send(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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); + + it("will support tracing batch and send", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + + const list = [{ name: "Albert" }, { name: "Marie" }]; + + const producer = client.createProducer({ partitionId: "0" }); + + const eventDataBatch = await producer.createBatch(); + for (let i = 0; i < 2; i++) { + eventDataBatch.tryAdd({ body: `${list[i].name}` }, { parentSpan: rootSpan }); + } + await producer.send(eventDataBatch, { parentSpan: 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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); it("with partition key should be sent successfully.", async function(): Promise { const producer = client.createProducer(); @@ -460,6 +612,121 @@ describe("EventHub Sender #RunnableInBrowser", function(): void { await client.createProducer({ partitionId: "0" }).send([{ body: "Hello World EventHub!!" }]); debug("Sent the message successfully on the same link.."); }); + + it("can be manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + + const producer = client.createProducer({ partitionId: "0" }); + + const events = []; + for (let i = 0; i < 5; i++) { + events.push({ body: `multiple messages - manual trace propgation: ${i}` }); + } + await producer.send(events, { parentSpan: 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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); + + it("skips already instrumented events when manually traced", async function(): Promise { + const tracer = new TestTracer(); + setTracer(tracer); + + const rootSpan = tracer.startSpan("root"); + + const producer = client.createProducer({ partitionId: "0" }); + + 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 producer.send(events, { parentSpan: 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.context().traceId).should.eql(expectedGraph); + tracer.getActiveSpans().length.should.equal(0, "All spans should have had end called."); + + await producer.close(); + }); }); describe("Negative scenarios", function(): void {