@devopsplaybook.io/otel-utils 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/ShutdownHooks.d.ts +16 -0
- package/dist/src/ShutdownHooks.js +36 -0
- package/dist/src/StandardLogger.d.ts +5 -1
- package/dist/src/StandardLogger.js +74 -26
- package/dist/src/StandardMeter.d.ts +4 -0
- package/dist/src/StandardMeter.js +42 -2
- package/dist/src/StandardTracer.d.ts +4 -0
- package/dist/src/StandardTracer.js +63 -44
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/ShutdownHooks.spec.ts +75 -0
- package/src/ShutdownHooks.ts +45 -0
- package/src/StandardLogger.spec.ts +126 -18
- package/src/StandardLogger.ts +85 -30
- package/src/StandardMeter.spec.ts +116 -20
- package/src/StandardMeter.ts +49 -2
- package/src/StandardTracer.spec.ts +187 -20
- package/src/StandardTracer.ts +76 -11
package/src/StandardMeter.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Counter,
|
|
3
3
|
Histogram,
|
|
4
4
|
Meter,
|
|
5
|
+
metrics,
|
|
5
6
|
ObservableGauge,
|
|
6
7
|
UpDownCounter,
|
|
7
8
|
} from "@opentelemetry/api";
|
|
@@ -13,6 +14,11 @@ import {
|
|
|
13
14
|
} from "@opentelemetry/sdk-metrics";
|
|
14
15
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
15
16
|
|
|
17
|
+
let globalMeterProvider: MeterProvider | undefined;
|
|
18
|
+
|
|
19
|
+
const ALREADY_REGISTERED_WARNING =
|
|
20
|
+
"[otel-utils] A global meter provider is already registered; StandardMeter will not replace it. This instance still records metrics through its own provider, but metrics.getMeter() keeps using the existing provider.";
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
23
|
* Options accepted by {@link StandardMeter.createHistogram}.
|
|
18
24
|
*/
|
|
@@ -29,11 +35,15 @@ export class StandardMeter {
|
|
|
29
35
|
private meterProvider: MeterProvider;
|
|
30
36
|
private serviceVersion: string;
|
|
31
37
|
private serviceName: string;
|
|
38
|
+
private ownsGlobalProvider = false;
|
|
39
|
+
private shutdownPromise?: Promise<void>;
|
|
32
40
|
|
|
33
41
|
constructor(config: ConfigOTelInterface) {
|
|
34
42
|
this.serviceName = config.SERVICE_ID;
|
|
35
43
|
this.serviceVersion = config.VERSION;
|
|
44
|
+
let hasCollector = false;
|
|
36
45
|
if (config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS) {
|
|
46
|
+
hasCollector = true;
|
|
37
47
|
const collectorOptions = {
|
|
38
48
|
url: config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS,
|
|
39
49
|
headers: {} as Record<string, string>,
|
|
@@ -63,10 +73,16 @@ export class StandardMeter {
|
|
|
63
73
|
this.meter = this.meterProvider.getMeter(
|
|
64
74
|
`${this.serviceName}:${this.serviceVersion}`,
|
|
65
75
|
);
|
|
76
|
+
if (hasCollector) {
|
|
77
|
+
this.registerGlobalProvider();
|
|
78
|
+
}
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
public
|
|
69
|
-
|
|
81
|
+
public shutdown(): Promise<void> {
|
|
82
|
+
if (!this.shutdownPromise) {
|
|
83
|
+
this.shutdownPromise = this.doShutdown();
|
|
84
|
+
}
|
|
85
|
+
return this.shutdownPromise;
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
public createCounter(key: string): Counter {
|
|
@@ -99,4 +115,35 @@ export class StandardMeter {
|
|
|
99
115
|
observableGauge.addCallback(callback);
|
|
100
116
|
return observableGauge;
|
|
101
117
|
}
|
|
118
|
+
|
|
119
|
+
private registerGlobalProvider(): void {
|
|
120
|
+
if (globalMeterProvider) {
|
|
121
|
+
if (globalMeterProvider === metrics.getMeterProvider()) {
|
|
122
|
+
console.warn(ALREADY_REGISTERED_WARNING);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// The previous provider is no longer registered (disabled elsewhere).
|
|
126
|
+
globalMeterProvider = undefined;
|
|
127
|
+
}
|
|
128
|
+
if (metrics.setGlobalMeterProvider(this.meterProvider)) {
|
|
129
|
+
globalMeterProvider = this.meterProvider;
|
|
130
|
+
this.ownsGlobalProvider = true;
|
|
131
|
+
} else {
|
|
132
|
+
console.warn(ALREADY_REGISTERED_WARNING);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async doShutdown(): Promise<void> {
|
|
137
|
+
await this.meterProvider.shutdown();
|
|
138
|
+
if (
|
|
139
|
+
this.ownsGlobalProvider &&
|
|
140
|
+
metrics.getMeterProvider() === this.meterProvider
|
|
141
|
+
) {
|
|
142
|
+
metrics.disable();
|
|
143
|
+
}
|
|
144
|
+
if (globalMeterProvider === this.meterProvider) {
|
|
145
|
+
globalMeterProvider = undefined;
|
|
146
|
+
}
|
|
147
|
+
this.ownsGlobalProvider = false;
|
|
148
|
+
}
|
|
102
149
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
diag,
|
|
3
|
+
DiagLogLevel,
|
|
4
|
+
SpanKind,
|
|
5
|
+
trace,
|
|
6
|
+
} from "@opentelemetry/api";
|
|
2
7
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8
|
+
import { TracerProvider } from "@opentelemetry/sdk-trace";
|
|
3
9
|
import {
|
|
4
10
|
ATTR_HTTP_REQUEST_METHOD,
|
|
5
11
|
ATTR_HTTP_ROUTE,
|
|
6
12
|
} from "@opentelemetry/semantic-conventions";
|
|
13
|
+
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
7
14
|
import { StandardTracer } from "./StandardTracer";
|
|
8
15
|
|
|
9
16
|
jest.mock("@opentelemetry/exporter-trace-otlp-http", () => {
|
|
@@ -12,53 +19,104 @@ jest.mock("@opentelemetry/exporter-trace-otlp-http", () => {
|
|
|
12
19
|
>("@opentelemetry/core");
|
|
13
20
|
return {
|
|
14
21
|
OTLPTraceExporter: jest.fn().mockImplementation(function () {
|
|
22
|
+
const exportedSpans: ExportedSpan[] = [];
|
|
15
23
|
return {
|
|
16
24
|
export: (
|
|
17
|
-
|
|
25
|
+
spans: ExportedSpan[],
|
|
18
26
|
resultCallback: (result: { code: number }) => void,
|
|
19
|
-
) =>
|
|
27
|
+
) => {
|
|
28
|
+
exportedSpans.push(...spans);
|
|
29
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
30
|
+
},
|
|
20
31
|
forceFlush: jest.fn().mockResolvedValue(undefined),
|
|
21
32
|
shutdown: jest.fn().mockResolvedValue(undefined),
|
|
33
|
+
getExportedSpans: () => exportedSpans,
|
|
22
34
|
};
|
|
23
35
|
}),
|
|
24
36
|
};
|
|
25
37
|
});
|
|
26
38
|
|
|
39
|
+
interface ExportedSpan {
|
|
40
|
+
name: string;
|
|
41
|
+
resource: { attributes: Record<string, unknown> };
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
interface MockTraceExporter {
|
|
28
45
|
export: jest.Mock;
|
|
29
46
|
forceFlush: jest.Mock;
|
|
30
47
|
shutdown: jest.Mock;
|
|
48
|
+
getExportedSpans: () => ExportedSpan[];
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
const mockExporterConstructor = OTLPTraceExporter as unknown as jest.Mock;
|
|
34
52
|
|
|
53
|
+
function exporterAt(index: number): MockTraceExporter {
|
|
54
|
+
return mockExporterConstructor.mock.results[index]
|
|
55
|
+
.value as MockTraceExporter;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
function lastExporter(): MockTraceExporter {
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
return exporterAt(mockExporterConstructor.mock.results.length - 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function registeredTracerProvider(): unknown {
|
|
63
|
+
const provider = trace.getTracerProvider() as unknown as {
|
|
64
|
+
getDelegate?: () => unknown;
|
|
65
|
+
};
|
|
66
|
+
return typeof provider.getDelegate === "function"
|
|
67
|
+
? provider.getDelegate()
|
|
68
|
+
: provider;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tracerConfig(
|
|
72
|
+
serviceId: string,
|
|
73
|
+
url?: string,
|
|
74
|
+
): ConfigOTelInterface {
|
|
75
|
+
return {
|
|
76
|
+
SERVICE_ID: serviceId,
|
|
77
|
+
VERSION: "1.0.0",
|
|
78
|
+
OPENTELEMETRY_COLLECTOR_HTTP_TRACES: url,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function spanNames(exporter: MockTraceExporter): string[] {
|
|
83
|
+
return exporter.getExportedSpans().map((span) => span.name);
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
describe("StandardTracer", () => {
|
|
41
|
-
|
|
87
|
+
const createdTracers: StandardTracer[] = [];
|
|
88
|
+
let warnSpy: jest.SpyInstance;
|
|
42
89
|
|
|
43
|
-
|
|
44
|
-
tracer = new StandardTracer(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
90
|
+
function makeTracer(config: ConfigOTelInterface): StandardTracer {
|
|
91
|
+
const tracer = new StandardTracer(config);
|
|
92
|
+
createdTracers.push(tracer);
|
|
93
|
+
return tracer;
|
|
94
|
+
}
|
|
49
95
|
|
|
50
96
|
beforeEach(() => {
|
|
51
97
|
jest.clearAllMocks();
|
|
98
|
+
trace.disable();
|
|
99
|
+
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(async () => {
|
|
103
|
+
for (const tracer of createdTracers.splice(0)) {
|
|
104
|
+
await tracer.shutdown();
|
|
105
|
+
}
|
|
106
|
+
trace.disable();
|
|
107
|
+
warnSpy.mockRestore();
|
|
52
108
|
});
|
|
53
109
|
|
|
54
110
|
describe("startSpan", () => {
|
|
55
111
|
it("sanitizes span names", () => {
|
|
112
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
56
113
|
const span = tracer.startSpan("GET-/api/files/:id");
|
|
57
114
|
expect(span.name).toBe("GET-/api/files/_id");
|
|
58
115
|
span.end();
|
|
59
116
|
});
|
|
60
117
|
|
|
61
118
|
it("keeps the default parentless behavior when no options are given", () => {
|
|
119
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
62
120
|
const span = tracer.startSpan("my-operation");
|
|
63
121
|
expect(span.kind).toBe(SpanKind.INTERNAL);
|
|
64
122
|
expect(span.attributes[ATTR_HTTP_REQUEST_METHOD]).toBe("BACKEND");
|
|
@@ -69,6 +127,7 @@ describe("StandardTracer", () => {
|
|
|
69
127
|
});
|
|
70
128
|
|
|
71
129
|
it("creates a child of the parent span without synthetic attributes", () => {
|
|
130
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
72
131
|
const parent = tracer.startSpan("parent-operation");
|
|
73
132
|
const child = tracer.startSpan("child-operation", parent);
|
|
74
133
|
expect(child.spanContext().traceId).toBe(parent.spanContext().traceId);
|
|
@@ -80,6 +139,7 @@ describe("StandardTracer", () => {
|
|
|
80
139
|
});
|
|
81
140
|
|
|
82
141
|
it("applies the options instead of the synthetic attributes", () => {
|
|
142
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
83
143
|
const span = tracer.startSpan("GET-/api/files/:id", undefined, {
|
|
84
144
|
kind: SpanKind.SERVER,
|
|
85
145
|
});
|
|
@@ -90,6 +150,7 @@ describe("StandardTracer", () => {
|
|
|
90
150
|
});
|
|
91
151
|
|
|
92
152
|
it("combines the options with a parent span", () => {
|
|
153
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
93
154
|
const parent = tracer.startSpan("parent-operation");
|
|
94
155
|
const child = tracer.startSpan("child-operation", parent, {
|
|
95
156
|
kind: SpanKind.CLIENT,
|
|
@@ -104,6 +165,7 @@ describe("StandardTracer", () => {
|
|
|
104
165
|
|
|
105
166
|
describe("updateHttpHeader", () => {
|
|
106
167
|
it("injects the W3C trace context into a new headers object", () => {
|
|
168
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
107
169
|
const span = tracer.startSpan("propagated");
|
|
108
170
|
const headers = StandardTracer.updateHttpHeader(span);
|
|
109
171
|
|
|
@@ -113,6 +175,7 @@ describe("StandardTracer", () => {
|
|
|
113
175
|
});
|
|
114
176
|
|
|
115
177
|
it("injects into and returns the provided headers object", () => {
|
|
178
|
+
const tracer = makeTracer(tracerConfig("test-service"));
|
|
116
179
|
const span = tracer.startSpan("propagated");
|
|
117
180
|
const headers: Record<string, string> = { "x-custom": "value" };
|
|
118
181
|
const result = StandardTracer.updateHttpHeader(span, headers);
|
|
@@ -124,9 +187,9 @@ describe("StandardTracer", () => {
|
|
|
124
187
|
});
|
|
125
188
|
});
|
|
126
189
|
|
|
127
|
-
describe("
|
|
190
|
+
describe("exporter configuration", () => {
|
|
128
191
|
it("builds the trace exporter with URL and auth header and shuts it down", async () => {
|
|
129
|
-
const exportTracer =
|
|
192
|
+
const exportTracer = makeTracer({
|
|
130
193
|
SERVICE_ID: "export-service",
|
|
131
194
|
VERSION: "2.0.0",
|
|
132
195
|
OPENTELEMETRY_COLLECTOR_HTTP_TRACES: "http://collector:4318/v1/traces",
|
|
@@ -143,7 +206,7 @@ describe("StandardTracer", () => {
|
|
|
143
206
|
});
|
|
144
207
|
|
|
145
208
|
it("builds the trace exporter without auth header", async () => {
|
|
146
|
-
const exportTracer =
|
|
209
|
+
const exportTracer = makeTracer({
|
|
147
210
|
SERVICE_ID: "export-service",
|
|
148
211
|
VERSION: "2.0.0",
|
|
149
212
|
OPENTELEMETRY_COLLECTOR_HTTP_TRACES: "http://collector:4318/v1/traces",
|
|
@@ -157,12 +220,116 @@ describe("StandardTracer", () => {
|
|
|
157
220
|
});
|
|
158
221
|
|
|
159
222
|
it("resolves without a collector", async () => {
|
|
160
|
-
const plainTracer =
|
|
161
|
-
SERVICE_ID: "plain-service",
|
|
162
|
-
VERSION: "1.0.0",
|
|
163
|
-
});
|
|
164
|
-
|
|
223
|
+
const plainTracer = makeTracer(tracerConfig("plain-service"));
|
|
165
224
|
await expect(plainTracer.shutdown()).resolves.toBeUndefined();
|
|
166
225
|
});
|
|
167
226
|
});
|
|
227
|
+
|
|
228
|
+
describe("provider ownership and global registration", () => {
|
|
229
|
+
it("routes each instance to its own exporter and resource", async () => {
|
|
230
|
+
const first = makeTracer(
|
|
231
|
+
tracerConfig("svc-1", "http://collector-1/v1/traces"),
|
|
232
|
+
);
|
|
233
|
+
const second = makeTracer(
|
|
234
|
+
tracerConfig("svc-2", "http://collector-2/v1/traces"),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
first.startSpan("span-from-svc1").end();
|
|
238
|
+
second.startSpan("span-from-svc2").end();
|
|
239
|
+
|
|
240
|
+
await first.shutdown();
|
|
241
|
+
await second.shutdown();
|
|
242
|
+
|
|
243
|
+
const firstSpans = exporterAt(0).getExportedSpans();
|
|
244
|
+
const secondSpans = exporterAt(1).getExportedSpans();
|
|
245
|
+
expect(firstSpans.map((span) => span.name)).toEqual(["span-from-svc1"]);
|
|
246
|
+
expect(secondSpans.map((span) => span.name)).toEqual(["span-from-svc2"]);
|
|
247
|
+
expect(firstSpans[0].resource.attributes["service.name"]).toBe("svc-1");
|
|
248
|
+
expect(secondSpans[0].resource.attributes["service.name"]).toBe("svc-2");
|
|
249
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
250
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
251
|
+
expect.stringContaining("A global tracer provider is already registered"),
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("routes global API spans to the first registered provider", async () => {
|
|
256
|
+
const first = makeTracer(
|
|
257
|
+
tracerConfig("svc-1", "http://collector-1/v1/traces"),
|
|
258
|
+
);
|
|
259
|
+
const second = makeTracer(
|
|
260
|
+
tracerConfig("svc-2", "http://collector-2/v1/traces"),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
trace.getTracer("global").startSpan("global-span").end();
|
|
264
|
+
second.startSpan("second-span").end();
|
|
265
|
+
|
|
266
|
+
await first.shutdown();
|
|
267
|
+
await second.shutdown();
|
|
268
|
+
|
|
269
|
+
expect(spanNames(exporterAt(0))).toEqual(["global-span"]);
|
|
270
|
+
expect(spanNames(exporterAt(1))).toEqual(["second-span"]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("only the owner disables the global provider and re-registration works", async () => {
|
|
274
|
+
const first = makeTracer(
|
|
275
|
+
tracerConfig("svc-1", "http://collector-1/v1/traces"),
|
|
276
|
+
);
|
|
277
|
+
const second = makeTracer(
|
|
278
|
+
tracerConfig("svc-2", "http://collector-2/v1/traces"),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
await second.shutdown();
|
|
282
|
+
expect(registeredTracerProvider()).toBeInstanceOf(TracerProvider);
|
|
283
|
+
|
|
284
|
+
trace.getTracer("global").startSpan("after-second-shutdown").end();
|
|
285
|
+
await first.shutdown();
|
|
286
|
+
expect(registeredTracerProvider()).not.toBeInstanceOf(TracerProvider);
|
|
287
|
+
expect(spanNames(exporterAt(0))).toContain("after-second-shutdown");
|
|
288
|
+
|
|
289
|
+
const third = makeTracer(
|
|
290
|
+
tracerConfig("svc-3", "http://collector-3/v1/traces"),
|
|
291
|
+
);
|
|
292
|
+
trace.getTracer("global").startSpan("after-reinit").end();
|
|
293
|
+
await third.shutdown();
|
|
294
|
+
|
|
295
|
+
expect(spanNames(exporterAt(2))).toEqual(["after-reinit"]);
|
|
296
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("shuts down idempotently", async () => {
|
|
300
|
+
const tracer = makeTracer(
|
|
301
|
+
tracerConfig("svc-1", "http://collector-1/v1/traces"),
|
|
302
|
+
);
|
|
303
|
+
tracer.startSpan("only-span").end();
|
|
304
|
+
|
|
305
|
+
await tracer.shutdown();
|
|
306
|
+
await tracer.shutdown();
|
|
307
|
+
|
|
308
|
+
expect(exporterAt(0).shutdown).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(spanNames(exporterAt(0))).toEqual(["only-span"]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("emits no duplicate-registration diagnostics for a normal startup", async () => {
|
|
313
|
+
const errors: unknown[][] = [];
|
|
314
|
+
diag.setLogger(
|
|
315
|
+
{
|
|
316
|
+
error: (...args: unknown[]) => errors.push(args),
|
|
317
|
+
warn: () => {},
|
|
318
|
+
info: () => {},
|
|
319
|
+
debug: () => {},
|
|
320
|
+
verbose: () => {},
|
|
321
|
+
},
|
|
322
|
+
DiagLogLevel.ERROR,
|
|
323
|
+
);
|
|
324
|
+
try {
|
|
325
|
+
makeTracer(tracerConfig("svc-1", "http://collector-1/v1/traces"));
|
|
326
|
+
makeTracer(tracerConfig("svc-2", "http://collector-2/v1/traces"));
|
|
327
|
+
} finally {
|
|
328
|
+
diag.disable();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
expect(errors).toEqual([]);
|
|
332
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
168
335
|
});
|
package/src/StandardTracer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
context,
|
|
2
3
|
defaultTextMapSetter,
|
|
3
4
|
ROOT_CONTEXT,
|
|
4
5
|
SpanKind,
|
|
@@ -22,6 +23,21 @@ import {
|
|
|
22
23
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
23
24
|
import { createOTelResource } from "./utils/createResource";
|
|
24
25
|
|
|
26
|
+
let globalTracerProvider: TracerProvider | undefined;
|
|
27
|
+
let contextManagerInitialized = false;
|
|
28
|
+
|
|
29
|
+
function getRegisteredTracerProvider(): unknown {
|
|
30
|
+
const provider = trace.getTracerProvider() as unknown as {
|
|
31
|
+
getDelegate?: () => unknown;
|
|
32
|
+
};
|
|
33
|
+
return typeof provider.getDelegate === "function"
|
|
34
|
+
? provider.getDelegate()
|
|
35
|
+
: provider;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ALREADY_REGISTERED_WARNING =
|
|
39
|
+
"[otel-utils] A global tracer provider is already registered; StandardTracer will not replace it. This instance still exports spans through its own provider, but trace.getTracer() keeps using the existing provider.";
|
|
40
|
+
|
|
25
41
|
/**
|
|
26
42
|
* Options accepted by {@link StandardTracer.startSpan}.
|
|
27
43
|
*/
|
|
@@ -44,6 +60,8 @@ export class StandardTracer {
|
|
|
44
60
|
private tracer: Tracer;
|
|
45
61
|
private serviceVersion: string;
|
|
46
62
|
private serviceName: string;
|
|
63
|
+
private ownsGlobalProvider = false;
|
|
64
|
+
private shutdownPromise?: Promise<void>;
|
|
47
65
|
|
|
48
66
|
constructor(config: ConfigOTelInterface) {
|
|
49
67
|
this.serviceName = config.SERVICE_ID;
|
|
@@ -67,19 +85,20 @@ export class StandardTracer {
|
|
|
67
85
|
resource: createOTelResource(this.serviceName, this.serviceVersion),
|
|
68
86
|
spanProcessors,
|
|
69
87
|
});
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const contextManager = new AsyncLocalStorageContextManager();
|
|
74
|
-
contextManager.enable();
|
|
75
|
-
opentelemetry.context.setGlobalContextManager(contextManager);
|
|
76
|
-
this.tracer = opentelemetry.trace.getTracer(
|
|
88
|
+
// Resolve the tracer from this instance's provider: the global API may be
|
|
89
|
+
// owned by another instance and would silently export through it.
|
|
90
|
+
this.tracer = this.traceProvider.getTracer(
|
|
77
91
|
`${this.serviceName}:${this.serviceVersion}`,
|
|
78
92
|
);
|
|
93
|
+
this.registerGlobalProvider();
|
|
94
|
+
initializeContextManager();
|
|
79
95
|
}
|
|
80
96
|
|
|
81
|
-
public
|
|
82
|
-
|
|
97
|
+
public shutdown(): Promise<void> {
|
|
98
|
+
if (!this.shutdownPromise) {
|
|
99
|
+
this.shutdownPromise = this.doShutdown();
|
|
100
|
+
}
|
|
101
|
+
return this.shutdownPromise;
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
public startSpan(
|
|
@@ -95,7 +114,7 @@ export class StandardTracer {
|
|
|
95
114
|
return this.tracer.startSpan(
|
|
96
115
|
sanitizedName,
|
|
97
116
|
options,
|
|
98
|
-
|
|
117
|
+
trace.setSpan(context.active(), parentSpan),
|
|
99
118
|
) as Span;
|
|
100
119
|
}
|
|
101
120
|
if (options) {
|
|
@@ -120,4 +139,50 @@ export class StandardTracer {
|
|
|
120
139
|
);
|
|
121
140
|
return headers;
|
|
122
141
|
}
|
|
142
|
+
|
|
143
|
+
private registerGlobalProvider(): void {
|
|
144
|
+
if (globalTracerProvider) {
|
|
145
|
+
if (getRegisteredTracerProvider() === globalTracerProvider) {
|
|
146
|
+
console.warn(ALREADY_REGISTERED_WARNING);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// The previous provider is no longer registered (disabled elsewhere).
|
|
150
|
+
globalTracerProvider = undefined;
|
|
151
|
+
}
|
|
152
|
+
if (trace.setGlobalTracerProvider(this.traceProvider)) {
|
|
153
|
+
globalTracerProvider = this.traceProvider;
|
|
154
|
+
this.ownsGlobalProvider = true;
|
|
155
|
+
} else {
|
|
156
|
+
console.warn(ALREADY_REGISTERED_WARNING);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async doShutdown(): Promise<void> {
|
|
161
|
+
await this.traceProvider.shutdown();
|
|
162
|
+
if (
|
|
163
|
+
this.ownsGlobalProvider &&
|
|
164
|
+
getRegisteredTracerProvider() === this.traceProvider
|
|
165
|
+
) {
|
|
166
|
+
trace.disable();
|
|
167
|
+
}
|
|
168
|
+
if (globalTracerProvider === this.traceProvider) {
|
|
169
|
+
globalTracerProvider = undefined;
|
|
170
|
+
}
|
|
171
|
+
this.ownsGlobalProvider = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function initializeContextManager(): void {
|
|
176
|
+
if (contextManagerInitialized) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
contextManagerInitialized = true;
|
|
180
|
+
const contextManager = new AsyncLocalStorageContextManager();
|
|
181
|
+
if (context.setGlobalContextManager(contextManager)) {
|
|
182
|
+
contextManager.enable();
|
|
183
|
+
} else {
|
|
184
|
+
console.warn(
|
|
185
|
+
"[otel-utils] A global context manager is already registered; StandardTracer will keep using it for context propagation.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
123
188
|
}
|