Skip to content

Commit ae40b6b

Browse files
authored
[core-http] Add createSpan helper (Azure#12525)
Add helper to create a span using the global tracer. Many of our Track2 SDKs implement their own createSpan, this helper should remove the need to re-implement it for every SDK
1 parent 266f9f3 commit ae40b6b

File tree

9 files changed

+341
-1
lines changed

9 files changed

+341
-1
lines changed

sdk/core/core-client/review/core-client.api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Pipeline } from '@azure/core-https';
1212
import { PipelinePolicy } from '@azure/core-https';
1313
import { PipelineRequest } from '@azure/core-https';
1414
import { PipelineResponse } from '@azure/core-https';
15+
import { Span } from '@opentelemetry/api';
1516
import { TokenCredential } from '@azure/core-auth';
1617
import { TransferProgressEvent } from '@azure/core-https';
1718

@@ -62,6 +63,12 @@ export function createSerializer(modelMappers?: {
6263
[key: string]: any;
6364
}, isXML?: boolean): Serializer;
6465

66+
// @public
67+
export function createSpanFunction({ packagePrefix, namespace }: SpanConfig): <T extends OperationOptions>(operationName: string, operationOptions: T) => {
68+
span: Span;
69+
updatedOptions: T;
70+
};
71+
6572
// @public
6673
export interface DeserializationContentTypes {
6774
json?: string[];
@@ -330,6 +337,12 @@ export interface SimpleMapperType {
330337
name: "Base64Url" | "Boolean" | "ByteArray" | "Date" | "DateTime" | "DateTimeRfc1123" | "Object" | "Stream" | "String" | "TimeSpan" | "UnixTime" | "Uuid" | "Number" | "any";
331338
}
332339

340+
// @public
341+
export interface SpanConfig {
342+
namespace: string;
343+
packagePrefix: string;
344+
}
345+
333346

334347
// (No @packageDocumentation comment for this package)
335348

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { Span, SpanOptions, SpanKind } from "@opentelemetry/api";
5+
import { getTracer } from "@azure/core-tracing";
6+
import { OperationOptions, SpanConfig } from "./interfaces";
7+
8+
type OperationTracingOptions = OperationOptions["tracingOptions"];
9+
10+
/**
11+
* Creates a function called createSpan to create spans using the global tracer.
12+
* @ignore
13+
* @param spanConfig The name of the operation being performed.
14+
* @param tracingOptions The options for the underlying http request.
15+
*/
16+
export function createSpanFunction({ packagePrefix, namespace }: SpanConfig) {
17+
return function<T extends OperationOptions>(
18+
operationName: string,
19+
operationOptions: T
20+
): { span: Span; updatedOptions: T } {
21+
const tracer = getTracer();
22+
const tracingOptions = operationOptions.tracingOptions || {};
23+
const spanOptions: SpanOptions = {
24+
...tracingOptions.spanOptions,
25+
kind: SpanKind.INTERNAL
26+
};
27+
28+
const span = tracer.startSpan(`${packagePrefix}.${operationName}`, spanOptions);
29+
30+
span.setAttribute("az.namespace", namespace);
31+
32+
let newSpanOptions = tracingOptions.spanOptions || {};
33+
if (span.isRecording()) {
34+
newSpanOptions = {
35+
...tracingOptions.spanOptions,
36+
parent: span.context(),
37+
attributes: {
38+
...spanOptions.attributes,
39+
"az.namespace": namespace
40+
}
41+
};
42+
}
43+
44+
const newTracingOptions: OperationTracingOptions = {
45+
...tracingOptions,
46+
spanOptions: newSpanOptions
47+
};
48+
49+
const newOperationOptions: T = {
50+
...operationOptions,
51+
tracingOptions: newTracingOptions
52+
};
53+
54+
return {
55+
span,
56+
updatedOptions: newOperationOptions
57+
};
58+
};
59+
}

sdk/core/core-client/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
export { createSerializer, MapperTypeNames } from "./serializer";
5+
export { createSpanFunction } from "./createSpan";
56
export { ServiceClient, ServiceClientOptions } from "./serviceClient";
67
export {
78
OperationSpec,
@@ -32,7 +33,8 @@ export {
3233
ParameterPath,
3334
OperationResponse,
3435
FullOperationResponse,
35-
PolymorphicDiscriminator
36+
PolymorphicDiscriminator,
37+
SpanConfig
3638
} from "./interfaces";
3739
export {
3840
deserializationPolicy,

sdk/core/core-client/src/interfaces.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,17 @@ export interface UrlParameterValue {
467467
value: string;
468468
skipUrlEncoding: boolean;
469469
}
470+
471+
/**
472+
* Configuration for creating a new Tracing Span
473+
*/
474+
export interface SpanConfig {
475+
/**
476+
* Package name prefix
477+
*/
478+
packagePrefix: string;
479+
/**
480+
* Service namespace
481+
*/
482+
namespace: string;
483+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { assert } from "chai";
5+
import { SpanKind, TraceFlags } from "@opentelemetry/api";
6+
import { setTracer, TestSpan, TestTracer } from "@azure/core-tracing";
7+
import sinon from "sinon";
8+
import { createSpanFunction } from "../src/createSpan";
9+
import { OperationOptions } from "../src/interfaces";
10+
11+
const createSpan = createSpanFunction({ namespace: "Microsoft.Test", packagePrefix: "Azure.Test" });
12+
13+
describe("createSpan", () => {
14+
it("returns a created span with the right metadata", () => {
15+
const tracer = new TestTracer();
16+
const testSpan = new TestSpan(
17+
tracer,
18+
"testing",
19+
{ traceId: "", spanId: "", traceFlags: TraceFlags.NONE },
20+
SpanKind.INTERNAL
21+
);
22+
const setAttributeSpy = sinon.spy(testSpan, "setAttribute");
23+
const startSpanStub = sinon.stub(tracer, "startSpan");
24+
startSpanStub.returns(testSpan);
25+
setTracer(tracer);
26+
const { span } = createSpan("testMethod", {});
27+
assert.strictEqual(span, testSpan, "Should return mocked span");
28+
assert.isTrue(startSpanStub.calledOnce);
29+
const [name, options] = startSpanStub.firstCall.args;
30+
assert.strictEqual(name, "Azure.Test.testMethod");
31+
assert.deepEqual(options, { kind: SpanKind.INTERNAL });
32+
assert.isTrue(setAttributeSpy.calledOnceWithExactly("az.namespace", "Microsoft.Test"));
33+
});
34+
35+
it("returns updated SpanOptions", () => {
36+
const options: OperationOptions = {};
37+
const { span, updatedOptions } = createSpan("testMethod", options);
38+
assert.isEmpty(options, "original options should not be modified");
39+
assert.notStrictEqual(updatedOptions, options, "should return new object");
40+
const expected: OperationOptions = {
41+
tracingOptions: {
42+
spanOptions: {
43+
parent: span.context(),
44+
attributes: {
45+
"az.namespace": "Microsoft.Test"
46+
}
47+
}
48+
}
49+
};
50+
assert.deepEqual(updatedOptions, expected);
51+
});
52+
53+
it("preserves existing attributes", () => {
54+
const options: OperationOptions = {
55+
tracingOptions: {
56+
spanOptions: {
57+
attributes: {
58+
foo: "bar"
59+
}
60+
}
61+
}
62+
};
63+
const { span, updatedOptions } = createSpan("testMethod", options);
64+
assert.notStrictEqual(updatedOptions, options, "should return new object");
65+
const expected: OperationOptions = {
66+
tracingOptions: {
67+
spanOptions: {
68+
parent: span.context(),
69+
attributes: {
70+
"az.namespace": "Microsoft.Test",
71+
foo: "bar"
72+
}
73+
}
74+
}
75+
};
76+
assert.deepEqual(updatedOptions, expected);
77+
});
78+
79+
afterEach(() => {
80+
sinon.restore();
81+
});
82+
});

sdk/core/core-http/review/core-http.api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Debugger } from '@azure/logger';
1010
import { GetTokenOptions } from '@azure/core-auth';
1111
import { isTokenCredential } from '@azure/core-auth';
1212
import { OperationTracingOptions } from '@azure/core-tracing';
13+
import { Span } from '@opentelemetry/api';
1314
import { SpanOptions } from '@azure/core-tracing';
1415
import { TokenCredential } from '@azure/core-auth';
1516

@@ -156,6 +157,12 @@ export const Constants: {
156157
// @public (undocumented)
157158
export function createPipelineFromOptions(pipelineOptions: InternalPipelineOptions, authPolicyFactory?: RequestPolicyFactory): ServiceClientOptions;
158159

160+
// @public
161+
export function createSpanFunction({ packagePrefix, namespace }: SpanConfig): <T extends OperationOptions>(operationName: string, operationOptions: T) => {
162+
span: Span;
163+
updatedOptions: T;
164+
};
165+
159166
// Warning: (ae-forgotten-export) The symbol "FetchHttpClient" needs to be exported by the entry point coreHttp.d.ts
160167
//
161168
// @public (undocumented)
@@ -790,6 +797,12 @@ export interface SimpleMapperType {
790797
name: "Base64Url" | "Boolean" | "ByteArray" | "Date" | "DateTime" | "DateTimeRfc1123" | "Object" | "Stream" | "String" | "TimeSpan" | "UnixTime" | "Uuid" | "Number" | "any";
791798
}
792799

800+
// @public
801+
export interface SpanConfig {
802+
namespace: string;
803+
packagePrefix: string;
804+
}
805+
793806
// @public
794807
export function stringifyXML(obj: any, opts?: SerializerOptions): string;
795808

sdk/core/core-http/src/coreHttp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export {
4343
ProxySettings,
4444
ProxyOptions
4545
} from "./serviceClient";
46+
export { createSpanFunction, SpanConfig } from "./createSpan";
4647
export { PipelineOptions, InternalPipelineOptions } from "./pipelineOptions";
4748
export { QueryCollectionFormat } from "./queryCollectionFormat";
4849
export { Constants } from "./util/constants";
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { Span, SpanOptions, SpanKind } from "@opentelemetry/api";
5+
import { getTracer } from "@azure/core-tracing";
6+
import { OperationOptions } from "./coreHttp";
7+
8+
type OperationTracingOptions = OperationOptions["tracingOptions"];
9+
10+
/**
11+
* Configuration for creating a new Tracing Span
12+
*/
13+
export interface SpanConfig {
14+
/**
15+
* Package name prefix
16+
*/
17+
packagePrefix: string;
18+
/**
19+
* Service namespace
20+
*/
21+
namespace: string;
22+
}
23+
24+
/**
25+
* Creates a function called createSpan to create spans using the global tracer.
26+
* @ignore
27+
* @param spanConfig The name of the operation being performed.
28+
* @param tracingOptions The options for the underlying http request.
29+
*/
30+
export function createSpanFunction({ packagePrefix, namespace }: SpanConfig) {
31+
return function<T extends OperationOptions>(
32+
operationName: string,
33+
operationOptions: T
34+
): { span: Span; updatedOptions: T } {
35+
const tracer = getTracer();
36+
const tracingOptions = operationOptions.tracingOptions || {};
37+
const spanOptions: SpanOptions = {
38+
...tracingOptions.spanOptions,
39+
kind: SpanKind.INTERNAL
40+
};
41+
42+
const span = tracer.startSpan(`${packagePrefix}.${operationName}`, spanOptions);
43+
44+
span.setAttribute("az.namespace", namespace);
45+
46+
let newSpanOptions = tracingOptions.spanOptions || {};
47+
if (span.isRecording()) {
48+
newSpanOptions = {
49+
...tracingOptions.spanOptions,
50+
parent: span.context(),
51+
attributes: {
52+
...spanOptions.attributes,
53+
"az.namespace": namespace
54+
}
55+
};
56+
}
57+
58+
const newTracingOptions: OperationTracingOptions = {
59+
...tracingOptions,
60+
spanOptions: newSpanOptions
61+
};
62+
63+
const newOperationOptions: T = {
64+
...operationOptions,
65+
tracingOptions: newTracingOptions
66+
};
67+
68+
return {
69+
span,
70+
updatedOptions: newOperationOptions
71+
};
72+
};
73+
}

0 commit comments

Comments
 (0)