[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:
Jeff Fisher 2022-04-11 14:57:13 -07:00 коммит произвёл GitHub
Родитель 2230ad9cc7
Коммит 35f7320720
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 314 добавлений и 614 удалений

Просмотреть файл

@ -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", () => {