[core-rest-pipeline] Update to core-tracing 1.0.0 GA (#21264)
Migrate core-rest-pipeline to core-tracing 1.0 GA and expose new type guard for RestError.
This commit is contained in:
Родитель
2230ad9cc7
Коммит
35f7320720
|
@ -279,6 +279,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "sdk/purview/purview-administration-rest"
|
"path": "sdk/purview/purview-administration-rest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "sdk/instrumentation/opentelemetry-instrumentation-azure-sdk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
|
@ -4,12 +4,18 @@
|
||||||
|
|
||||||
### Features Added
|
### Features Added
|
||||||
|
|
||||||
|
- Exposed type guard for RestError called `isRestError` for typesafe exception handling.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
### Bugs Fixed
|
### Bugs Fixed
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
- Updated our `@azure/core-tracing` dependency to the latest version (1.0.0).
|
||||||
|
- Notable changes include Removal of `@opentelemetry/api` as a transitive dependency and ensuring that the active context is properly propagated.
|
||||||
|
- Customers who would like to continue using OpenTelemetry driven tracing should visit our [OpenTelemetry Instrumentation](https://www.npmjs.com/package/@azure/opentelemetry-instrumentation-azure-sdk) package for instructions.
|
||||||
|
|
||||||
## 1.8.0 (2022-03-31)
|
## 1.8.0 (2022-03-31)
|
||||||
|
|
||||||
### Features Added
|
### Features Added
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"@azure/abort-controller": "^1.0.0",
|
||||||
"@azure/core-auth": "^1.3.0",
|
"@azure/core-auth": "^1.3.0",
|
||||||
"@azure/core-tracing": "1.0.0-preview.13",
|
"@azure/core-tracing": "^1.0.0",
|
||||||
"@azure/logger": "^1.0.0",
|
"@azure/logger": "^1.0.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.2.0",
|
||||||
|
|
|
@ -150,6 +150,9 @@ export interface InternalPipelineOptions extends PipelineOptions {
|
||||||
loggingOptions?: LogPolicyOptions;
|
loggingOptions?: LogPolicyOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export function isRestError(e: unknown): e is RestError;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function logPolicy(options?: LogPolicyOptions): PipelinePolicy;
|
export function logPolicy(options?: LogPolicyOptions): PipelinePolicy;
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export {
|
||||||
export { createDefaultHttpClient } from "./defaultHttpClient";
|
export { createDefaultHttpClient } from "./defaultHttpClient";
|
||||||
export { createHttpHeaders } from "./httpHeaders";
|
export { createHttpHeaders } from "./httpHeaders";
|
||||||
export { createPipelineRequest, PipelineRequestOptions } from "./pipelineRequest";
|
export { createPipelineRequest, PipelineRequestOptions } from "./pipelineRequest";
|
||||||
export { RestError, RestErrorOptions } from "./restError";
|
export { RestError, RestErrorOptions, isRestError } from "./restError";
|
||||||
export {
|
export {
|
||||||
decompressResponsePolicy,
|
decompressResponsePolicy,
|
||||||
decompressResponsePolicyName,
|
decompressResponsePolicyName,
|
||||||
|
|
|
@ -2,23 +2,18 @@
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Span,
|
TracingSpan,
|
||||||
SpanOptions,
|
createTracingClient,
|
||||||
SpanStatusCode,
|
TracingClient,
|
||||||
createSpanFunction,
|
TracingContext,
|
||||||
getTraceParentHeader,
|
|
||||||
isSpanContextValid,
|
|
||||||
} from "@azure/core-tracing";
|
} from "@azure/core-tracing";
|
||||||
import { SpanKind } from "@azure/core-tracing";
|
import { SDK_VERSION } from "../constants";
|
||||||
import { PipelineRequest, PipelineResponse, SendRequest } from "../interfaces";
|
import { PipelineRequest, PipelineResponse, SendRequest } from "../interfaces";
|
||||||
import { PipelinePolicy } from "../pipeline";
|
import { PipelinePolicy } from "../pipeline";
|
||||||
import { getUserAgentValue } from "../util/userAgent";
|
import { getUserAgentValue } from "../util/userAgent";
|
||||||
import { logger } from "../log";
|
import { logger } from "../log";
|
||||||
|
import { isError, getErrorMessage } from "../util/helpers";
|
||||||
const createSpan = createSpanFunction({
|
import { isRestError } from "../restError";
|
||||||
packagePrefix: "",
|
|
||||||
namespace: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The programmatic identifier of the tracingPolicy.
|
* The programmatic identifier of the tracingPolicy.
|
||||||
|
@ -45,22 +40,23 @@ export interface TracingPolicyOptions {
|
||||||
*/
|
*/
|
||||||
export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolicy {
|
export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolicy {
|
||||||
const userAgent = getUserAgentValue(options.userAgentPrefix);
|
const userAgent = getUserAgentValue(options.userAgentPrefix);
|
||||||
|
const tracingClient = tryCreateTracingClient();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: tracingPolicyName,
|
name: tracingPolicyName,
|
||||||
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
|
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
|
||||||
if (!request.tracingOptions?.tracingContext) {
|
if (!tracingClient || !request.tracingOptions?.tracingContext) {
|
||||||
return next(request);
|
return next(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
const span = tryCreateSpan(request, userAgent);
|
const { span, tracingContext } = tryCreateSpan(tracingClient, request, userAgent) ?? {};
|
||||||
|
|
||||||
if (!span) {
|
if (!span || !tracingContext) {
|
||||||
return next(request);
|
return next(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await next(request);
|
const response = await tracingClient.withContext(tracingContext, next, request);
|
||||||
tryProcessResponse(span, response);
|
tryProcessResponse(span, response);
|
||||||
return response;
|
return response;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -71,19 +67,38 @@ export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolic
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryCreateSpan(request: PipelineRequest, userAgent?: string): Span | undefined {
|
function tryCreateTracingClient(): TracingClient | undefined {
|
||||||
try {
|
try {
|
||||||
const createSpanOptions: SpanOptions = {
|
return createTracingClient({
|
||||||
...(request.tracingOptions as any)?.spanOptions,
|
namespace: "",
|
||||||
kind: SpanKind.CLIENT,
|
packageName: "@azure/core-rest-pipeline",
|
||||||
};
|
packageVersion: SDK_VERSION,
|
||||||
|
|
||||||
// Passing spanOptions as part of tracingOptions to maintain compatibility @azure/core-tracing@preview.13 and earlier.
|
|
||||||
// We can pass this as a separate parameter once we upgrade to the latest core-tracing.
|
|
||||||
// As per spec, we do not need to differentiate between HTTP and HTTPS in span name.
|
|
||||||
const { span } = createSpan(`HTTP ${request.method}`, {
|
|
||||||
tracingOptions: { ...request.tracingOptions, spanOptions: createSpanOptions },
|
|
||||||
});
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logger.warning(`Error when creating the TracingClient: ${getErrorMessage(e)}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryCreateSpan(
|
||||||
|
tracingClient: TracingClient,
|
||||||
|
request: PipelineRequest,
|
||||||
|
userAgent?: string
|
||||||
|
): { span: TracingSpan; tracingContext: TracingContext } | undefined {
|
||||||
|
try {
|
||||||
|
// As per spec, we do not need to differentiate between HTTP and HTTPS in span name.
|
||||||
|
const { span, updatedOptions } = tracingClient.startSpan(
|
||||||
|
`HTTP ${request.method}`,
|
||||||
|
{ tracingOptions: request.tracingOptions },
|
||||||
|
{
|
||||||
|
spanKind: "client",
|
||||||
|
spanAttributes: {
|
||||||
|
"http.method": request.method,
|
||||||
|
"http.url": request.url,
|
||||||
|
requestId: request.requestId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// If the span is not recording, don't do any more work.
|
// If the span is not recording, don't do any more work.
|
||||||
if (!span.isRecording()) {
|
if (!span.isRecording()) {
|
||||||
|
@ -91,58 +106,40 @@ function tryCreateSpan(request: PipelineRequest, userAgent?: string): Span | und
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const namespaceFromContext = request.tracingOptions?.tracingContext?.getValue(
|
|
||||||
Symbol.for("az.namespace")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof namespaceFromContext === "string") {
|
|
||||||
span.setAttribute("az.namespace", namespaceFromContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
span.setAttributes({
|
|
||||||
"http.method": request.method,
|
|
||||||
"http.url": request.url,
|
|
||||||
requestId: request.requestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userAgent) {
|
if (userAgent) {
|
||||||
span.setAttribute("http.user_agent", userAgent);
|
span.setAttribute("http.user_agent", userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set headers
|
// set headers
|
||||||
const spanContext = span.spanContext();
|
const headers = tracingClient.createRequestHeaders(
|
||||||
const traceParentHeader = getTraceParentHeader(spanContext);
|
updatedOptions.tracingOptions.tracingContext
|
||||||
if (traceParentHeader && isSpanContextValid(spanContext)) {
|
);
|
||||||
request.headers.set("traceparent", traceParentHeader);
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
const traceState = spanContext.traceState && spanContext.traceState.serialize();
|
request.headers.set(key, value);
|
||||||
// if tracestate is set, traceparent MUST be set, so only set tracestate after traceparent
|
|
||||||
if (traceState) {
|
|
||||||
request.headers.set("tracestate", traceState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return span;
|
return { span, tracingContext: updatedOptions.tracingOptions.tracingContext };
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
logger.warning(`Skipping creating a tracing span due to an error: ${error.message}`);
|
logger.warning(`Skipping creating a tracing span due to an error: ${getErrorMessage(e)}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryProcessError(span: Span, err: any): void {
|
function tryProcessError(span: TracingSpan, error: unknown): void {
|
||||||
try {
|
try {
|
||||||
span.setStatus({
|
span.setStatus({
|
||||||
code: SpanStatusCode.ERROR,
|
status: "error",
|
||||||
message: err.message,
|
error: isError(error) ? error : undefined,
|
||||||
});
|
});
|
||||||
if (err.statusCode) {
|
if (isRestError(error) && error.statusCode) {
|
||||||
span.setAttribute("http.status_code", err.statusCode);
|
span.setAttribute("http.status_code", error.statusCode);
|
||||||
}
|
}
|
||||||
span.end();
|
span.end();
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
logger.warning(`Skipping tracing span processing due to an error: ${error.message}`);
|
logger.warning(`Skipping tracing span processing due to an error: ${getErrorMessage(e)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryProcessResponse(span: Span, response: PipelineResponse): void {
|
function tryProcessResponse(span: TracingSpan, response: PipelineResponse): void {
|
||||||
try {
|
try {
|
||||||
span.setAttribute("http.status_code", response.status);
|
span.setAttribute("http.status_code", response.status);
|
||||||
const serviceRequestId = response.headers.get("x-ms-request-id");
|
const serviceRequestId = response.headers.get("x-ms-request-id");
|
||||||
|
@ -150,10 +147,10 @@ function tryProcessResponse(span: Span, response: PipelineResponse): void {
|
||||||
span.setAttribute("serviceRequestId", serviceRequestId);
|
span.setAttribute("serviceRequestId", serviceRequestId);
|
||||||
}
|
}
|
||||||
span.setStatus({
|
span.setStatus({
|
||||||
code: SpanStatusCode.OK,
|
status: "success",
|
||||||
});
|
});
|
||||||
span.end();
|
span.end();
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
logger.warning(`Skipping tracing span processing due to an error: ${error.message}`);
|
logger.warning(`Skipping tracing span processing due to an error: ${getErrorMessage(e)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { PipelineRequest, PipelineResponse } from "./interfaces";
|
import { PipelineRequest, PipelineResponse } from "./interfaces";
|
||||||
import { custom } from "./util/inspect";
|
import { custom } from "./util/inspect";
|
||||||
import { Sanitizer } from "./util/sanitizer";
|
import { Sanitizer } from "./util/sanitizer";
|
||||||
|
import { isError } from "./util/helpers";
|
||||||
|
|
||||||
const errorSanitizer = new Sanitizer();
|
const errorSanitizer = new Sanitizer();
|
||||||
|
|
||||||
|
@ -84,3 +85,14 @@ export class RestError extends Error {
|
||||||
return `RestError: ${this.message} \n ${errorSanitizer.sanitize(this)}`;
|
return `RestError: ${this.message} \n ${errorSanitizer.sanitize(this)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typeguard for RestError
|
||||||
|
* @param e Something caught by a catch clause.
|
||||||
|
*/
|
||||||
|
export function isRestError(e: unknown): e is RestError {
|
||||||
|
if (e instanceof RestError) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isError(e) && e.name === "RestError";
|
||||||
|
}
|
||||||
|
|
|
@ -122,3 +122,42 @@ export function parseHeaderValueAsNumber(
|
||||||
if (Number.isNaN(valueAsNum)) return;
|
if (Number.isNaN(valueAsNum)) return;
|
||||||
return valueAsNum;
|
return valueAsNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Typeguard for an error object shape (has name and message)
|
||||||
|
* @param e Something caught by a catch clause.
|
||||||
|
*/
|
||||||
|
export function isError(e: unknown): e is Error {
|
||||||
|
if (isObject(e)) {
|
||||||
|
const hasName = typeof e.name === "string";
|
||||||
|
const hasMessage = typeof e.message === "string";
|
||||||
|
return hasName && hasMessage;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Given what is thought to be an error object, return the message if possible.
|
||||||
|
* If the message is missing, returns a stringified version of the input.
|
||||||
|
* @param e Something thrown from a try{} block
|
||||||
|
* @returns The error message or a string of the input
|
||||||
|
*/
|
||||||
|
export function getErrorMessage(e: unknown): string {
|
||||||
|
if (isError(e)) {
|
||||||
|
return e.message;
|
||||||
|
} else {
|
||||||
|
let stringified: string;
|
||||||
|
try {
|
||||||
|
if (typeof e === "object" && e) {
|
||||||
|
stringified = JSON.stringify(e);
|
||||||
|
} else {
|
||||||
|
stringified = String(e);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
stringified = "[unable to stringify input]";
|
||||||
|
}
|
||||||
|
return `Unknown error ${stringified}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { assert } from "chai";
|
import { assert } from "chai";
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import {
|
import {
|
||||||
|
PipelineRequest,
|
||||||
PipelineResponse,
|
PipelineResponse,
|
||||||
RestError,
|
RestError,
|
||||||
SendRequest,
|
SendRequest,
|
||||||
|
@ -12,217 +13,117 @@ import {
|
||||||
tracingPolicy,
|
tracingPolicy,
|
||||||
} from "../src";
|
} from "../src";
|
||||||
import {
|
import {
|
||||||
SpanAttributeValue,
|
Instrumenter,
|
||||||
SpanAttributes,
|
InstrumenterSpanOptions,
|
||||||
SpanContext,
|
|
||||||
SpanOptions,
|
|
||||||
SpanStatus,
|
SpanStatus,
|
||||||
SpanStatusCode,
|
TracingContext,
|
||||||
TraceFlags,
|
TracingSpan,
|
||||||
TraceState,
|
TracingSpanOptions,
|
||||||
context,
|
useInstrumenter,
|
||||||
setSpan,
|
|
||||||
} from "@azure/core-tracing";
|
} from "@azure/core-tracing";
|
||||||
import { Span, Tracer, TracerProvider, trace } from "@opentelemetry/api";
|
|
||||||
|
|
||||||
export class MockSpan implements Span {
|
class MockSpan implements TracingSpan {
|
||||||
private _endCalled = false;
|
spanAttributes: Record<string, unknown> = {};
|
||||||
private _status: SpanStatus = {
|
endCalled: boolean = false;
|
||||||
code: SpanStatusCode.UNSET,
|
status?: SpanStatus;
|
||||||
};
|
exceptions: Array<Error | string> = [];
|
||||||
private _attributes: SpanAttributes = {};
|
|
||||||
|
|
||||||
constructor(
|
constructor(public name: string, spanOptions: TracingSpanOptions = {}) {
|
||||||
private name: string,
|
this.spanAttributes = spanOptions.spanAttributes ?? {};
|
||||||
private traceId: string,
|
|
||||||
private spanId: string,
|
|
||||||
private flags: TraceFlags,
|
|
||||||
private state: string,
|
|
||||||
options?: SpanOptions
|
|
||||||
) {
|
|
||||||
this._attributes = options?.attributes || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
addEvent(): this {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isRecording(): boolean {
|
isRecording(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordException(): void {
|
recordException(exception: Error | string): void {
|
||||||
throw new Error("Method not implemented.");
|
this.exceptions.push(exception);
|
||||||
}
|
|
||||||
|
|
||||||
updateName(): this {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
didEnd(): boolean {
|
|
||||||
return this._endCalled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end(): void {
|
end(): void {
|
||||||
this._endCalled = true;
|
this.endCalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): SpanStatus {
|
setStatus(status: SpanStatus): void {
|
||||||
return this._status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status: SpanStatus): this {
|
setAttribute(name: string, value: unknown): void {
|
||||||
this._status = status;
|
this.spanAttributes[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(name: string): unknown {
|
||||||
|
return this.spanAttributes[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noopTracingContext: TracingContext = {
|
||||||
|
deleteValue() {
|
||||||
return this;
|
return this;
|
||||||
}
|
},
|
||||||
|
getValue() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
setValue() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
setAttributes(attributes: SpanAttributes): this {
|
class MockInstrumenter implements Instrumenter {
|
||||||
for (const key in attributes) {
|
lastSpanCreated: MockSpan | undefined;
|
||||||
this.setAttribute(key, attributes[key]!);
|
staticSpan: MockSpan | undefined;
|
||||||
|
|
||||||
|
setStaticSpan(span: MockSpan): void {
|
||||||
|
this.staticSpan = span;
|
||||||
|
}
|
||||||
|
startSpan(
|
||||||
|
name: string,
|
||||||
|
spanOptions: InstrumenterSpanOptions
|
||||||
|
): {
|
||||||
|
span: TracingSpan;
|
||||||
|
tracingContext: TracingContext;
|
||||||
|
} {
|
||||||
|
const tracingContext = spanOptions.tracingContext ?? noopTracingContext;
|
||||||
|
if (this.staticSpan) {
|
||||||
|
return { span: this.staticSpan, tracingContext };
|
||||||
}
|
}
|
||||||
return this;
|
const span = new MockSpan(name, spanOptions);
|
||||||
}
|
this.lastSpanCreated = span;
|
||||||
|
|
||||||
setAttribute(key: string, value: SpanAttributeValue): this {
|
|
||||||
this._attributes[key] = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName(): string {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttribute(key: string): SpanAttributeValue | undefined {
|
|
||||||
return this._attributes[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
spanContext(): SpanContext {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
const traceState = {
|
|
||||||
set(): TraceState {
|
|
||||||
/* empty */
|
|
||||||
return traceState;
|
|
||||||
},
|
|
||||||
unset(): TraceState {
|
|
||||||
/* empty */
|
|
||||||
return traceState;
|
|
||||||
},
|
|
||||||
get(): string | undefined {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
serialize() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
traceId: this.traceId,
|
span,
|
||||||
spanId: this.spanId,
|
tracingContext,
|
||||||
traceFlags: this.flags,
|
|
||||||
traceState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
withContext<
|
||||||
|
CallbackArgs extends unknown[],
|
||||||
export class MockTracer implements Tracer {
|
Callback extends (...args: CallbackArgs) => ReturnType<Callback>
|
||||||
private spans: MockSpan[] = [];
|
>(
|
||||||
private _startSpanCalled = false;
|
_context: TracingContext,
|
||||||
|
callback: Callback,
|
||||||
constructor(
|
...callbackArgs: CallbackArgs
|
||||||
private traceId = "",
|
): ReturnType<Callback> {
|
||||||
private spanId = "",
|
return callback(...callbackArgs);
|
||||||
private flags = TraceFlags.NONE,
|
|
||||||
private state = ""
|
|
||||||
) {}
|
|
||||||
|
|
||||||
startActiveSpan(): never {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStartedSpans(): MockSpan[] {
|
parseTraceparentHeader(_traceparentHeader: string): TracingContext | undefined {
|
||||||
return this.spans;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
createRequestHeaders(_tracingContext?: TracingContext): Record<string, string> {
|
||||||
startSpanCalled(): boolean {
|
return {};
|
||||||
return this._startSpanCalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
startSpan(name: string, options?: SpanOptions): MockSpan {
|
|
||||||
this._startSpanCalled = true;
|
|
||||||
const span = new MockSpan(name, this.traceId, this.spanId, this.flags, this.state, options);
|
|
||||||
this.spans.push(span);
|
|
||||||
return span;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockTracerProvider implements TracerProvider {
|
|
||||||
private mockTracer: Tracer = new MockTracer();
|
|
||||||
|
|
||||||
setTracer(tracer: Tracer): void {
|
|
||||||
this.mockTracer = tracer;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTracer(): Tracer {
|
|
||||||
return this.mockTracer;
|
|
||||||
}
|
|
||||||
|
|
||||||
register(): void {
|
|
||||||
trace.setGlobalTracerProvider(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
disable(): void {
|
|
||||||
trace.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROOT_SPAN = new MockSpan("name", "root", "root", TraceFlags.SAMPLED, "");
|
|
||||||
|
|
||||||
describe("tracingPolicy", function () {
|
describe("tracingPolicy", function () {
|
||||||
const TRACE_VERSION = "00";
|
let activeInstrumenter: MockInstrumenter;
|
||||||
const mockTracerProvider = new MockTracerProvider();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockTracerProvider.register();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mockTracerProvider.disable();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("will not create a span if tracingContext is missing", async () => {
|
|
||||||
const mockTracer = new MockTracer();
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.isFalse(mockTracer.startSpanCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("will create a span with the correct data", async () => {
|
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
|
function createTestRequest({ noContext = false } = {}): {
|
||||||
|
request: PipelineRequest;
|
||||||
|
next: sinon.SinonStub;
|
||||||
|
} {
|
||||||
const request = createPipelineRequest({
|
const request = createPipelineRequest({
|
||||||
url: "https://bing.com",
|
url: "https://bing.com",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
tracingOptions: {
|
tracingOptions: { tracingContext: noContext ? undefined : noopTracingContext },
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN).setValue(
|
|
||||||
Symbol.for("az.namespace"),
|
|
||||||
"test"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: PipelineResponse = {
|
const response: PipelineResponse = {
|
||||||
|
@ -230,369 +131,110 @@ describe("tracingPolicy", function () {
|
||||||
request: request,
|
request: request,
|
||||||
status: 200,
|
status: 200,
|
||||||
};
|
};
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
||||||
next.resolves(response);
|
next.resolves(response);
|
||||||
await policy.sendRequest(request, next);
|
return { request, next };
|
||||||
|
}
|
||||||
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
afterEach(() => {
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
sinon.restore();
|
||||||
assert.equal(span.getName(), "HTTP POST");
|
|
||||||
assert.equal(span.getAttribute("az.namespace"), "test");
|
|
||||||
assert.equal(span.getAttribute("http.method"), "POST");
|
|
||||||
assert.equal(span.getAttribute("http.url"), request.url);
|
|
||||||
assert.equal(span.getAttribute("requestId"), request.requestId);
|
|
||||||
assert.equal(span.getAttribute("http.status_code"), response.status);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will create a span and correctly set trace headers if tracingContext is available", async () => {
|
beforeEach(() => {
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
activeInstrumenter = new MockInstrumenter();
|
||||||
const mockSpanId = "2222222222222222";
|
useInstrumenter(activeInstrumenter);
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.isTrue(span.didEnd());
|
|
||||||
assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK });
|
|
||||||
assert.equal(span.getAttribute("http.status_code"), 200);
|
|
||||||
|
|
||||||
const expectedFlag = "01";
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
request.headers.get("traceparent"),
|
|
||||||
`${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}`
|
|
||||||
);
|
|
||||||
assert.notExists(request.headers.get("tracestate"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will create a span and correctly set trace headers if tracingContext is available (no TraceOptions)", async () => {
|
it("will create a span with the correct data", async () => {
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
// leave out the TraceOptions
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
const policy = tracingPolicy();
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
const { request, next } = createTestRequest();
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
await policy.sendRequest(request, next);
|
||||||
|
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
const createdSpan = activeInstrumenter.lastSpanCreated;
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
assert.exists(createdSpan);
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
const mockSpan = createdSpan!;
|
||||||
assert.isTrue(span.didEnd());
|
assert.isTrue(mockSpan.endCalled, "expected span to be ended");
|
||||||
assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK });
|
assert.equal(mockSpan.name, "HTTP POST");
|
||||||
assert.equal(span.getAttribute("http.status_code"), 200);
|
assert.equal(mockSpan.getAttribute("http.method"), "POST");
|
||||||
|
assert.equal(mockSpan.getAttribute("http.url"), request.url);
|
||||||
const expectedFlag = "00";
|
assert.equal(mockSpan.getAttribute("requestId"), request.requestId);
|
||||||
|
assert.equal(mockSpan.getAttribute("http.status_code"), 200); // createTestRequest's response will return 200 OK
|
||||||
assert.equal(
|
|
||||||
request.headers.get("traceparent"),
|
|
||||||
`${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}`
|
|
||||||
);
|
|
||||||
assert.notExists(request.headers.get("tracestate"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will create a span and correctly set trace headers tracingContext is available (TraceState)", async () => {
|
it("will set request headers correctly", async () => {
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
sinon.stub(activeInstrumenter, "createRequestHeaders").returns({
|
||||||
const mockSpanId = "2222222222222222";
|
testheader: "testvalue",
|
||||||
const mockTraceState = "foo=bar";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
method: "PUT",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const response: PipelineResponse = {
|
const { request, next } = createTestRequest();
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
const policy = tracingPolicy();
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
await policy.sendRequest(request, next);
|
||||||
|
assert.equal(request.headers.get("testheader"), "testvalue");
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.isTrue(span.didEnd());
|
|
||||||
assert.deepEqual(span.getStatus(), { code: SpanStatusCode.OK });
|
|
||||||
|
|
||||||
const expectedFlag = "01";
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
request.headers.get("traceparent"),
|
|
||||||
`${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}`
|
|
||||||
);
|
|
||||||
assert.equal(request.headers.get("tracestate"), mockTraceState);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will close a span if an error is encountered", async () => {
|
it("will close a span if an error is encountered", async () => {
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
const mockTraceState = "foo=bar";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED, mockTraceState);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
const request = createPipelineRequest({
|
||||||
url: "https://bing.com",
|
url: "https://bing.com",
|
||||||
tracingOptions: {
|
tracingOptions: {
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
tracingContext: noopTracingContext,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const policy = tracingPolicy();
|
const policy = tracingPolicy();
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
||||||
next.rejects(new RestError("Bad Request.", { statusCode: 400 }));
|
const requestError = new RestError("Bad Request.", { statusCode: 400 });
|
||||||
|
next.rejects(requestError);
|
||||||
|
|
||||||
try {
|
await assert.isRejected(policy.sendRequest(request, next), requestError);
|
||||||
await policy.sendRequest(request, next);
|
const createdSpan = activeInstrumenter.lastSpanCreated;
|
||||||
throw new Error("Test Failure");
|
assert.exists(createdSpan);
|
||||||
} catch (err) {
|
const mockSpan = createdSpan!;
|
||||||
assert.notEqual(err.message, "Test Failure");
|
assert.equal(mockSpan.status?.status, "error");
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
if (mockSpan.status?.status === "error") {
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
assert.equal(mockSpan.status?.error, requestError);
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.isTrue(span.didEnd());
|
|
||||||
assert.deepEqual(span.getStatus(), {
|
|
||||||
code: SpanStatusCode.ERROR,
|
|
||||||
message: "Bad Request.",
|
|
||||||
});
|
|
||||||
assert.equal(span.getAttribute("http.status_code"), 400);
|
|
||||||
|
|
||||||
const expectedFlag = "01";
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
request.headers.get("traceparent"),
|
|
||||||
`${TRACE_VERSION}-${mockTraceId}-${mockSpanId}-${expectedFlag}`
|
|
||||||
);
|
|
||||||
assert.equal(request.headers.get("tracestate"), mockTraceState);
|
|
||||||
}
|
}
|
||||||
|
assert.isTrue(mockSpan.endCalled, "end was expected to be called!");
|
||||||
|
assert.equal(mockSpan.getAttribute("http.status_code"), 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will not set headers if span is a NoOpSpan", async () => {
|
it("will not create a span if tracingContext is missing", async () => {
|
||||||
mockTracerProvider.disable();
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
const policy = tracingPolicy();
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
const { request, next } = createTestRequest({ noContext: true });
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
await policy.sendRequest(request, next);
|
||||||
|
|
||||||
assert.notExists(request.headers.get("traceparent"));
|
const createdSpan = activeInstrumenter.lastSpanCreated;
|
||||||
assert.notExists(request.headers.get("tracestate"));
|
assert.notExists(createdSpan, "span was created without tracingContext being passed!");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will not set headers if context is invalid", async () => {
|
describe("span errors", () => {
|
||||||
// This will create a tracer that produces invalid trace-id and span-id
|
it("will not fail the request when creating a span throws", async () => {
|
||||||
const mockTracer = new MockTracer("invalid", "00", TraceFlags.SAMPLED, "foo=bar");
|
sinon.stub(activeInstrumenter, "startSpan").throws("boom");
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
const { request, next } = createTestRequest();
|
||||||
|
const policy = tracingPolicy();
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
await assert.isFulfilled(policy.sendRequest(request, next));
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.notExists(request.headers.get("traceparent"));
|
it("will not fail the request when post-processing success fails", async () => {
|
||||||
assert.notExists(request.headers.get("tracestate"));
|
const mockSpan = sinon.createStubInstance(MockSpan);
|
||||||
});
|
mockSpan.end.throws(new Error("end is not a function"));
|
||||||
|
activeInstrumenter.setStaticSpan(mockSpan);
|
||||||
|
const { request, next } = createTestRequest();
|
||||||
|
const policy = tracingPolicy();
|
||||||
|
|
||||||
it("will not fail the request if span setup fails", async () => {
|
await assert.isFulfilled(policy.sendRequest(request, next));
|
||||||
const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, "");
|
|
||||||
sinon.stub(errorTracer, "startSpan").throws(new Error("Test Error"));
|
|
||||||
mockTracerProvider.setTracer(errorTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
|
|
||||||
// Does not throw
|
it("will not fail the request when post-processing error fails", async () => {
|
||||||
const result = await policy.sendRequest(request, next);
|
const mockSpan = sinon.createStubInstance(MockSpan);
|
||||||
assert.equal(result, response);
|
mockSpan.end.throws(new Error("end is not a function"));
|
||||||
});
|
const { request, next } = createTestRequest();
|
||||||
|
const policy = tracingPolicy();
|
||||||
|
const expectedError = new RestError("Bad Request.", { statusCode: 400 });
|
||||||
|
next.rejects(expectedError);
|
||||||
|
|
||||||
it("will not fail the request if response processing fails", async () => {
|
// Expect the pipeline request error, _not_ the error that is thrown when ending a span.
|
||||||
const errorTracer = new MockTracer("", "", TraceFlags.SAMPLED, "");
|
await assert.isRejected(policy.sendRequest(request, next), expectedError);
|
||||||
mockTracerProvider.setTracer(errorTracer);
|
|
||||||
const errorSpan = new MockSpan("", "", "", TraceFlags.SAMPLED, "");
|
|
||||||
sinon.stub(errorSpan, "end").throws(new Error("Test Error"));
|
|
||||||
sinon.stub(errorTracer, "startSpan").returns(errorSpan);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
|
|
||||||
// Does not throw
|
|
||||||
const result = await policy.sendRequest(request, next);
|
|
||||||
assert.equal(result, response);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("will give priority to context's az.namespace over spanOptions", async () => {
|
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN).setValue(
|
|
||||||
Symbol.for("az.namespace"),
|
|
||||||
"value_from_context"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Object.assign(request.tracingOptions, {
|
|
||||||
spanOptions: { attributes: { "az.namespace": "value_from_span_options" } },
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.equal(span.getAttribute("az.namespace"), "value_from_context");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("will use spanOptions if context does not have namespace", async () => {
|
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Object.assign(request.tracingOptions, {
|
|
||||||
spanOptions: { attributes: { "az.namespace": "value_from_span_options" } },
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.equal(span.getAttribute("az.namespace"), "value_from_span_options");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is robust when spanOptions is undefined", async () => {
|
|
||||||
const mockTraceId = "11111111111111111111111111111111";
|
|
||||||
const mockSpanId = "2222222222222222";
|
|
||||||
const mockTracer = new MockTracer(mockTraceId, mockSpanId, TraceFlags.SAMPLED);
|
|
||||||
mockTracerProvider.setTracer(mockTracer);
|
|
||||||
|
|
||||||
const request = createPipelineRequest({
|
|
||||||
url: "https://bing.com",
|
|
||||||
tracingOptions: {
|
|
||||||
tracingContext: setSpan(context.active(), ROOT_SPAN),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response: PipelineResponse = {
|
|
||||||
headers: createHttpHeaders(),
|
|
||||||
request: request,
|
|
||||||
status: 200,
|
|
||||||
};
|
|
||||||
const policy = tracingPolicy();
|
|
||||||
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
|
|
||||||
next.resolves(response);
|
|
||||||
await policy.sendRequest(request, next);
|
|
||||||
|
|
||||||
assert.isTrue(mockTracer.startSpanCalled());
|
|
||||||
assert.lengthOf(mockTracer.getStartedSpans(), 1);
|
|
||||||
const span = mockTracer.getStartedSpans()[0];
|
|
||||||
assert.notExists(span.getAttribute("az.namespace"));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
- Minor improvements to the typing of `updatedOptions` when calling startSpan.
|
||||||
|
|
||||||
## 1.0.0 (2022-03-31)
|
## 1.0.0 (2022-03-31)
|
||||||
|
|
||||||
This release marks the GA release of our @azure/core-tracing libraries and is unchanged from preview.14
|
This release marks the GA release of our @azure/core-tracing libraries and is unchanged from preview.14
|
||||||
|
|
|
@ -30,6 +30,13 @@ export interface OperationTracingOptions {
|
||||||
tracingContext?: TracingContext;
|
tracingContext?: TracingContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export type OptionsWithTracingContext<Options> = Options & {
|
||||||
|
tracingOptions: {
|
||||||
|
tracingContext: TracingContext;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type Resolved<T> = T extends {
|
export type Resolved<T> = T extends {
|
||||||
then(onfulfilled: infer F): any;
|
then(onfulfilled: infer F): any;
|
||||||
|
@ -57,7 +64,7 @@ export interface TracingClient {
|
||||||
tracingOptions?: OperationTracingOptions;
|
tracingOptions?: OperationTracingOptions;
|
||||||
}>(name: string, operationOptions?: Options, spanOptions?: TracingSpanOptions): {
|
}>(name: string, operationOptions?: Options, spanOptions?: TracingSpanOptions): {
|
||||||
span: TracingSpan;
|
span: TracingSpan;
|
||||||
updatedOptions: Options;
|
updatedOptions: OptionsWithTracingContext<Options>;
|
||||||
};
|
};
|
||||||
withContext<CallbackArgs extends unknown[], Callback extends (...args: CallbackArgs) => ReturnType<Callback>>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType<Callback>;
|
withContext<CallbackArgs extends unknown[], Callback extends (...args: CallbackArgs) => ReturnType<Callback>>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType<Callback>;
|
||||||
withSpan<Options extends {
|
withSpan<Options extends {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export {
|
||||||
Instrumenter,
|
Instrumenter,
|
||||||
InstrumenterSpanOptions,
|
InstrumenterSpanOptions,
|
||||||
OperationTracingOptions,
|
OperationTracingOptions,
|
||||||
|
OptionsWithTracingContext,
|
||||||
Resolved,
|
Resolved,
|
||||||
SpanStatus,
|
SpanStatus,
|
||||||
SpanStatusError,
|
SpanStatusError,
|
||||||
|
|
|
@ -62,7 +62,10 @@ export interface TracingClient {
|
||||||
name: string,
|
name: string,
|
||||||
operationOptions?: Options,
|
operationOptions?: Options,
|
||||||
spanOptions?: TracingSpanOptions
|
spanOptions?: TracingSpanOptions
|
||||||
): { span: TracingSpan; updatedOptions: Options };
|
): {
|
||||||
|
span: TracingSpan;
|
||||||
|
updatedOptions: OptionsWithTracingContext<Options>;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Wraps a callback with an active context and calls the callback.
|
* Wraps a callback with an active context and calls the callback.
|
||||||
* Depending on the implementation, this may set the globally available active context.
|
* Depending on the implementation, this may set the globally available active context.
|
||||||
|
@ -278,3 +281,13 @@ export interface OperationTracingOptions {
|
||||||
/** The context to use for created Tracing Spans. */
|
/** The context to use for created Tracing Spans. */
|
||||||
tracingContext?: TracingContext;
|
tracingContext?: TracingContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility type for when we know a TracingContext has been set
|
||||||
|
* as part of an operation's options.
|
||||||
|
*/
|
||||||
|
export type OptionsWithTracingContext<Options> = Options & {
|
||||||
|
tracingOptions: {
|
||||||
|
tracingContext: TracingContext;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OperationTracingOptions,
|
OperationTracingOptions,
|
||||||
|
OptionsWithTracingContext,
|
||||||
Resolved,
|
Resolved,
|
||||||
TracingClient,
|
TracingClient,
|
||||||
TracingClientOptions,
|
TracingClientOptions,
|
||||||
|
@ -28,7 +29,7 @@ export function createTracingClient(options: TracingClientOptions): TracingClien
|
||||||
spanOptions?: TracingSpanOptions
|
spanOptions?: TracingSpanOptions
|
||||||
): {
|
): {
|
||||||
span: TracingSpan;
|
span: TracingSpan;
|
||||||
updatedOptions: Options;
|
updatedOptions: OptionsWithTracingContext<Options>;
|
||||||
} {
|
} {
|
||||||
const startSpanResult = getInstrumenter().startSpan(name, {
|
const startSpanResult = getInstrumenter().startSpan(name, {
|
||||||
...spanOptions,
|
...spanOptions,
|
||||||
|
@ -42,12 +43,10 @@ export function createTracingClient(options: TracingClientOptions): TracingClien
|
||||||
tracingContext = tracingContext.setValue(knownContextKeys.namespace, namespace);
|
tracingContext = tracingContext.setValue(knownContextKeys.namespace, namespace);
|
||||||
}
|
}
|
||||||
span.setAttribute("az.namespace", tracingContext.getValue(knownContextKeys.namespace));
|
span.setAttribute("az.namespace", tracingContext.getValue(knownContextKeys.namespace));
|
||||||
const updatedOptions = {
|
const updatedOptions: OptionsWithTracingContext<Options> = Object.assign({}, operationOptions, {
|
||||||
...operationOptions,
|
tracingOptions: { ...operationOptions?.tracingOptions, tracingContext },
|
||||||
tracingOptions: {
|
});
|
||||||
tracingContext: tracingContext,
|
|
||||||
},
|
|
||||||
} as Options;
|
|
||||||
return {
|
return {
|
||||||
span,
|
span,
|
||||||
updatedOptions,
|
updatedOptions,
|
||||||
|
@ -68,7 +67,7 @@ export function createTracingClient(options: TracingClientOptions): TracingClien
|
||||||
): Promise<Resolved<ReturnType<Callback>>> {
|
): Promise<Resolved<ReturnType<Callback>>> {
|
||||||
const { span, updatedOptions } = startSpan(name, operationOptions, spanOptions);
|
const { span, updatedOptions } = startSpan(name, operationOptions, spanOptions);
|
||||||
try {
|
try {
|
||||||
const result = await withContext(updatedOptions.tracingOptions!.tracingContext!, () =>
|
const result = await withContext(updatedOptions.tracingOptions.tracingContext, () =>
|
||||||
Promise.resolve(callback(updatedOptions, span))
|
Promise.resolve(callback(updatedOptions, span))
|
||||||
);
|
);
|
||||||
span.setStatus({ status: "success" });
|
span.setStatus({ status: "success" });
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import { TracingClient, TracingContext, TracingSpan } from "./interfaces";
|
import { TracingContext, TracingSpan } from "./interfaces";
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const knownContextKeys = {
|
export const knownContextKeys = {
|
||||||
span: Symbol.for("@azure/core-tracing span"),
|
span: Symbol.for("@azure/core-tracing span"),
|
||||||
namespace: Symbol.for("@azure/core-tracing namespace"),
|
namespace: Symbol.for("@azure/core-tracing namespace"),
|
||||||
client: Symbol.for("@azure/core-tracing client"),
|
|
||||||
parentContext: Symbol.for("@azure/core-tracing parent context"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,9 +21,6 @@ export function createTracingContext(options: CreateTracingContextOptions = {}):
|
||||||
if (options.span) {
|
if (options.span) {
|
||||||
context = context.setValue(knownContextKeys.span, options.span);
|
context = context.setValue(knownContextKeys.span, options.span);
|
||||||
}
|
}
|
||||||
if (options.client) {
|
|
||||||
context = context.setValue(knownContextKeys.client, options.client);
|
|
||||||
}
|
|
||||||
if (options.namespace) {
|
if (options.namespace) {
|
||||||
context = context.setValue(knownContextKeys.namespace, options.namespace);
|
context = context.setValue(knownContextKeys.namespace, options.namespace);
|
||||||
}
|
}
|
||||||
|
@ -63,12 +58,10 @@ export class TracingContextImpl implements TracingContext {
|
||||||
* Represents a set of items that can be set when creating a new {@link TracingContext}.
|
* Represents a set of items that can be set when creating a new {@link TracingContext}.
|
||||||
*/
|
*/
|
||||||
export interface CreateTracingContextOptions {
|
export interface CreateTracingContextOptions {
|
||||||
/** The {@link parentContext} - the newly created context will contain all the values of the parent context unless overriden. */
|
/** The {@link parentContext} - the newly created context will contain all the values of the parent context unless overridden. */
|
||||||
parentContext?: TracingContext;
|
parentContext?: TracingContext;
|
||||||
/** An initial span to set on the context. */
|
/** An initial span to set on the context. */
|
||||||
span?: TracingSpan;
|
span?: TracingSpan;
|
||||||
/** The tracing client used to create this context. */
|
|
||||||
client?: TracingClient;
|
|
||||||
/** The namespace to set on any child spans. */
|
/** The namespace to set on any child spans. */
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,10 +85,18 @@ describe("TracingClient", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Returns tracingContext in updatedOptions", () => {
|
it("Returns tracingContext in updatedOptions", () => {
|
||||||
let { updatedOptions } = client.startSpan("test");
|
let { updatedOptions } = client.startSpan<{}>("test");
|
||||||
assert.exists(updatedOptions.tracingOptions?.tracingContext);
|
assert.exists(updatedOptions.tracingOptions.tracingContext);
|
||||||
updatedOptions = client.startSpan("test", updatedOptions).updatedOptions;
|
updatedOptions = client.startSpan("test", updatedOptions).updatedOptions;
|
||||||
assert.exists(updatedOptions.tracingOptions?.tracingContext);
|
assert.exists(updatedOptions.tracingOptions.tracingContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not erase unknown tracingOptions", () => {
|
||||||
|
// this test is to future-proof any tracingOptions we might add
|
||||||
|
let { updatedOptions } = client.startSpan<{}>("test", {
|
||||||
|
tracingOptions: { unknownProp: true } as any,
|
||||||
|
});
|
||||||
|
assert.exists((updatedOptions.tracingOptions as any).unknownProp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import { TracingContextImpl, createTracingContext, knownContextKeys } from "../src/tracingContext";
|
import { TracingContextImpl, createTracingContext, knownContextKeys } from "../src/tracingContext";
|
||||||
import { assert } from "chai";
|
import { assert } from "chai";
|
||||||
import { createDefaultTracingSpan } from "../src/instrumenter";
|
import { createDefaultTracingSpan } from "../src/instrumenter";
|
||||||
import { createTracingClient } from "../src/tracingClient";
|
|
||||||
|
|
||||||
describe("TracingContext", () => {
|
describe("TracingContext", () => {
|
||||||
describe("TracingContextImpl", () => {
|
describe("TracingContextImpl", () => {
|
||||||
|
@ -96,15 +95,12 @@ describe("TracingContext", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can add known attributes", () => {
|
it("can add known attributes", () => {
|
||||||
const client = createTracingClient({ namespace: "test", packageName: "test" });
|
|
||||||
const span = createDefaultTracingSpan();
|
const span = createDefaultTracingSpan();
|
||||||
const namespace = "test-namespace";
|
const namespace = "test-namespace";
|
||||||
const newContext = createTracingContext({
|
const newContext = createTracingContext({
|
||||||
client,
|
|
||||||
span,
|
span,
|
||||||
namespace,
|
namespace,
|
||||||
});
|
});
|
||||||
assert.strictEqual(newContext.getValue(knownContextKeys.client), client);
|
|
||||||
assert.strictEqual(newContext.getValue(knownContextKeys.namespace), namespace);
|
assert.strictEqual(newContext.getValue(knownContextKeys.namespace), namespace);
|
||||||
assert.strictEqual(newContext.getValue(knownContextKeys.span), span);
|
assert.strictEqual(newContext.getValue(knownContextKeys.span), span);
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,7 +46,7 @@ class AzureSdkInstrumentation extends InstrumentationBase {
|
||||||
/**
|
/**
|
||||||
* Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries.
|
* Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries.
|
||||||
*
|
*
|
||||||
* When registerd, any Azure data plane package will begin emitting tracing spans for internal calls
|
* When registered, any Azure data plane package will begin emitting tracing spans for internal calls
|
||||||
* as well as network calls
|
* as well as network calls
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
|
|
|
@ -58,7 +58,7 @@ class AzureSdkInstrumentation extends InstrumentationBase {
|
||||||
/**
|
/**
|
||||||
* Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries.
|
* Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries.
|
||||||
*
|
*
|
||||||
* When registerd, any Azure data plane package will begin emitting tracing spans for internal calls
|
* When registered, any Azure data plane package will begin emitting tracing spans for internal calls
|
||||||
* as well as network calls
|
* as well as network calls
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
|
|
|
@ -47,14 +47,6 @@ export class OpenTelemetryInstrumenter implements Instrumenter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMPAT: remove when core-rest-pipeline has upgraded to core-tracing 1.0
|
|
||||||
// https://github.com/Azure/azure-sdk-for-js/issues/20567
|
|
||||||
const newNamespaceKey = Symbol.for("@azure/core-tracing namespace");
|
|
||||||
const oldNamespaceKey = Symbol.for("az.namespace");
|
|
||||||
if (!ctx.getValue(oldNamespaceKey) && ctx.getValue(newNamespaceKey)) {
|
|
||||||
ctx = ctx.setValue(oldNamespaceKey, ctx.getValue(newNamespaceKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
span: new OpenTelemetrySpanWrapper(span),
|
span: new OpenTelemetrySpanWrapper(span),
|
||||||
tracingContext: trace.setSpan(ctx, span),
|
tracingContext: trace.setSpan(ctx, span),
|
||||||
|
|
|
@ -88,19 +88,6 @@ describe("OpenTelemetryInstrumenter", () => {
|
||||||
|
|
||||||
assert.equal(trace.getSpan(tracingContext), unwrap(span));
|
assert.equal(trace.getSpan(tracingContext), unwrap(span));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds az.namespace as a context attribute for compatibility", async () => {
|
|
||||||
const currentContext = context
|
|
||||||
.active()
|
|
||||||
.setValue(Symbol.for("@azure/core-tracing namespace"), "test-namespace");
|
|
||||||
|
|
||||||
const { tracingContext } = instrumenter.startSpan("test", {
|
|
||||||
tracingContext: currentContext,
|
|
||||||
packageName,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(tracingContext.getValue(Symbol.for("az.namespace")), "test-namespace");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a context is not provided", () => {
|
describe("when a context is not provided", () => {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче