@devopsplaybook.io/otel-utils 1.8.0-beta.39.883f25d → 1.9.0-beta.40.3ebb823

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.
@@ -1,349 +0,0 @@
1
- import {
2
- diag,
3
- DiagLogLevel,
4
- SpanKind,
5
- trace,
6
- } from "@opentelemetry/api";
7
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
8
- import { TracerProvider } from "@opentelemetry/sdk-trace";
9
- import {
10
- ATTR_HTTP_REQUEST_METHOD,
11
- ATTR_HTTP_ROUTE,
12
- } from "@opentelemetry/semantic-conventions";
13
- import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
14
- import { StandardTracer } from "./StandardTracer";
15
-
16
- jest.mock("@opentelemetry/exporter-trace-otlp-http", () => {
17
- const { ExportResultCode } = jest.requireActual<
18
- typeof import("@opentelemetry/core")
19
- >("@opentelemetry/core");
20
- return {
21
- OTLPTraceExporter: jest.fn().mockImplementation(function () {
22
- const exportedSpans: ExportedSpan[] = [];
23
- return {
24
- export: (
25
- spans: ExportedSpan[],
26
- resultCallback: (result: { code: number }) => void,
27
- ) => {
28
- exportedSpans.push(...spans);
29
- resultCallback({ code: ExportResultCode.SUCCESS });
30
- },
31
- forceFlush: jest.fn().mockResolvedValue(undefined),
32
- shutdown: jest.fn().mockResolvedValue(undefined),
33
- getExportedSpans: () => exportedSpans,
34
- };
35
- }),
36
- };
37
- });
38
-
39
- interface ExportedSpan {
40
- name: string;
41
- resource: { attributes: Record<string, unknown> };
42
- }
43
-
44
- interface MockTraceExporter {
45
- export: jest.Mock;
46
- forceFlush: jest.Mock;
47
- shutdown: jest.Mock;
48
- getExportedSpans: () => ExportedSpan[];
49
- }
50
-
51
- const mockExporterConstructor = OTLPTraceExporter as unknown as jest.Mock;
52
-
53
- function exporterAt(index: number): MockTraceExporter {
54
- return mockExporterConstructor.mock.results[index]
55
- .value as MockTraceExporter;
56
- }
57
-
58
- function lastExporter(): MockTraceExporter {
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);
84
- }
85
-
86
- describe("StandardTracer", () => {
87
- const createdTracers: StandardTracer[] = [];
88
- let warnSpy: jest.SpyInstance;
89
-
90
- function makeTracer(config: ConfigOTelInterface): StandardTracer {
91
- const tracer = new StandardTracer(config);
92
- createdTracers.push(tracer);
93
- return tracer;
94
- }
95
-
96
- beforeEach(() => {
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();
108
- });
109
-
110
- describe("startSpan", () => {
111
- it("sanitizes span names", () => {
112
- const tracer = makeTracer(tracerConfig("test-service"));
113
- const span = tracer.startSpan("GET-/api/files/:id");
114
- expect(span.name).toBe("GET-/api/files/_id");
115
- span.end();
116
- });
117
-
118
- it("keeps the default parentless behavior when no options are given", () => {
119
- const tracer = makeTracer(tracerConfig("test-service"));
120
- const span = tracer.startSpan("my-operation");
121
- expect(span.kind).toBe(SpanKind.INTERNAL);
122
- expect(span.attributes[ATTR_HTTP_REQUEST_METHOD]).toBe("BACKEND");
123
- expect(span.attributes[ATTR_HTTP_ROUTE]).toBe(
124
- "test-service-1.0.0-my-operation",
125
- );
126
- span.end();
127
- });
128
-
129
- it("creates a child of the parent span without synthetic attributes", () => {
130
- const tracer = makeTracer(tracerConfig("test-service"));
131
- const parent = tracer.startSpan("parent-operation");
132
- const child = tracer.startSpan("child-operation", parent);
133
- expect(child.spanContext().traceId).toBe(parent.spanContext().traceId);
134
- expect(child.parentSpanContext?.spanId).toBe(parent.spanContext().spanId);
135
- expect(child.attributes[ATTR_HTTP_REQUEST_METHOD]).toBeUndefined();
136
- expect(child.attributes[ATTR_HTTP_ROUTE]).toBeUndefined();
137
- child.end();
138
- parent.end();
139
- });
140
-
141
- it("applies the options instead of the synthetic attributes", () => {
142
- const tracer = makeTracer(tracerConfig("test-service"));
143
- const span = tracer.startSpan("GET-/api/files/:id", undefined, {
144
- kind: SpanKind.SERVER,
145
- });
146
- expect(span.kind).toBe(SpanKind.SERVER);
147
- expect(span.attributes[ATTR_HTTP_REQUEST_METHOD]).toBeUndefined();
148
- expect(span.attributes[ATTR_HTTP_ROUTE]).toBeUndefined();
149
- span.end();
150
- });
151
-
152
- it("combines the options with a parent span", () => {
153
- const tracer = makeTracer(tracerConfig("test-service"));
154
- const parent = tracer.startSpan("parent-operation");
155
- const child = tracer.startSpan("child-operation", parent, {
156
- kind: SpanKind.CLIENT,
157
- });
158
- expect(child.kind).toBe(SpanKind.CLIENT);
159
- expect(child.spanContext().traceId).toBe(parent.spanContext().traceId);
160
- expect(child.parentSpanContext?.spanId).toBe(parent.spanContext().spanId);
161
- child.end();
162
- parent.end();
163
- });
164
- });
165
-
166
- describe("updateHttpHeader", () => {
167
- it("injects the W3C trace context into a new headers object", () => {
168
- const tracer = makeTracer(tracerConfig("test-service"));
169
- const span = tracer.startSpan("propagated");
170
- const headers = StandardTracer.updateHttpHeader(span);
171
-
172
- expect(headers.traceparent).toMatch(/^00-/);
173
- expect(headers.traceparent).toContain(span.spanContext().traceId);
174
- span.end();
175
- });
176
-
177
- it("injects into and returns the provided headers object", () => {
178
- const tracer = makeTracer(tracerConfig("test-service"));
179
- const span = tracer.startSpan("propagated");
180
- const headers: Record<string, string> = { "x-custom": "value" };
181
- const result = StandardTracer.updateHttpHeader(span, headers);
182
-
183
- expect(result).toBe(headers);
184
- expect(headers["x-custom"]).toBe("value");
185
- expect(headers["traceparent"]).toContain(span.spanContext().traceId);
186
- span.end();
187
- });
188
- });
189
-
190
- describe("exporter configuration", () => {
191
- it("builds the trace exporter with URL and auth header and shuts it down", async () => {
192
- const exportTracer = makeTracer({
193
- SERVICE_ID: "export-service",
194
- VERSION: "2.0.0",
195
- OPENTELEMETRY_COLLECTOR_HTTP_TRACES: "http://collector:4318/v1/traces",
196
- OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER: "secret",
197
- });
198
-
199
- expect(mockExporterConstructor).toHaveBeenCalledWith({
200
- url: "http://collector:4318/v1/traces",
201
- headers: { Authorization: "Bearer secret" },
202
- });
203
-
204
- await exportTracer.shutdown();
205
- expect(lastExporter().shutdown).toHaveBeenCalled();
206
- });
207
-
208
- it("builds the trace exporter without auth header", async () => {
209
- const exportTracer = makeTracer({
210
- SERVICE_ID: "export-service",
211
- VERSION: "2.0.0",
212
- OPENTELEMETRY_COLLECTOR_HTTP_TRACES: "http://collector:4318/v1/traces",
213
- });
214
-
215
- expect(mockExporterConstructor).toHaveBeenCalledWith({
216
- url: "http://collector:4318/v1/traces",
217
- headers: {},
218
- });
219
- await exportTracer.shutdown();
220
- });
221
-
222
- it("resolves without a collector", async () => {
223
- const plainTracer = makeTracer(tracerConfig("plain-service"));
224
- await expect(plainTracer.shutdown()).resolves.toBeUndefined();
225
- });
226
- });
227
-
228
- describe("service identity validation", () => {
229
- it("rejects an empty SERVICE_ID", () => {
230
- expect(
231
- () => new StandardTracer({ SERVICE_ID: " ", VERSION: "1.0.0" }),
232
- ).toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
233
- });
234
-
235
- it("rejects an empty VERSION", () => {
236
- expect(
237
- () => new StandardTracer({ SERVICE_ID: "test-service", VERSION: "" }),
238
- ).toThrow("[otel-utils] VERSION must be a non-empty string");
239
- });
240
- });
241
-
242
- describe("provider ownership and global registration", () => {
243
- it("routes each instance to its own exporter and resource", async () => {
244
- const first = makeTracer(
245
- tracerConfig("svc-1", "http://collector-1/v1/traces"),
246
- );
247
- const second = makeTracer(
248
- tracerConfig("svc-2", "http://collector-2/v1/traces"),
249
- );
250
-
251
- first.startSpan("span-from-svc1").end();
252
- second.startSpan("span-from-svc2").end();
253
-
254
- await first.shutdown();
255
- await second.shutdown();
256
-
257
- const firstSpans = exporterAt(0).getExportedSpans();
258
- const secondSpans = exporterAt(1).getExportedSpans();
259
- expect(firstSpans.map((span) => span.name)).toEqual(["span-from-svc1"]);
260
- expect(secondSpans.map((span) => span.name)).toEqual(["span-from-svc2"]);
261
- expect(firstSpans[0].resource.attributes["service.name"]).toBe("svc-1");
262
- expect(secondSpans[0].resource.attributes["service.name"]).toBe("svc-2");
263
- expect(warnSpy).toHaveBeenCalledTimes(1);
264
- expect(warnSpy).toHaveBeenCalledWith(
265
- expect.stringContaining("A global tracer provider is already registered"),
266
- );
267
- });
268
-
269
- it("routes global API spans to the first registered provider", async () => {
270
- const first = makeTracer(
271
- tracerConfig("svc-1", "http://collector-1/v1/traces"),
272
- );
273
- const second = makeTracer(
274
- tracerConfig("svc-2", "http://collector-2/v1/traces"),
275
- );
276
-
277
- trace.getTracer("global").startSpan("global-span").end();
278
- second.startSpan("second-span").end();
279
-
280
- await first.shutdown();
281
- await second.shutdown();
282
-
283
- expect(spanNames(exporterAt(0))).toEqual(["global-span"]);
284
- expect(spanNames(exporterAt(1))).toEqual(["second-span"]);
285
- });
286
-
287
- it("only the owner disables the global provider and re-registration works", async () => {
288
- const first = makeTracer(
289
- tracerConfig("svc-1", "http://collector-1/v1/traces"),
290
- );
291
- const second = makeTracer(
292
- tracerConfig("svc-2", "http://collector-2/v1/traces"),
293
- );
294
-
295
- await second.shutdown();
296
- expect(registeredTracerProvider()).toBeInstanceOf(TracerProvider);
297
-
298
- trace.getTracer("global").startSpan("after-second-shutdown").end();
299
- await first.shutdown();
300
- expect(registeredTracerProvider()).not.toBeInstanceOf(TracerProvider);
301
- expect(spanNames(exporterAt(0))).toContain("after-second-shutdown");
302
-
303
- const third = makeTracer(
304
- tracerConfig("svc-3", "http://collector-3/v1/traces"),
305
- );
306
- trace.getTracer("global").startSpan("after-reinit").end();
307
- await third.shutdown();
308
-
309
- expect(spanNames(exporterAt(2))).toEqual(["after-reinit"]);
310
- expect(warnSpy).toHaveBeenCalledTimes(1);
311
- });
312
-
313
- it("shuts down idempotently", async () => {
314
- const tracer = makeTracer(
315
- tracerConfig("svc-1", "http://collector-1/v1/traces"),
316
- );
317
- tracer.startSpan("only-span").end();
318
-
319
- await tracer.shutdown();
320
- await tracer.shutdown();
321
-
322
- expect(exporterAt(0).shutdown).toHaveBeenCalledTimes(1);
323
- expect(spanNames(exporterAt(0))).toEqual(["only-span"]);
324
- });
325
-
326
- it("emits no duplicate-registration diagnostics for a normal startup", async () => {
327
- const errors: unknown[][] = [];
328
- diag.setLogger(
329
- {
330
- error: (...args: unknown[]) => errors.push(args),
331
- warn: () => {},
332
- info: () => {},
333
- debug: () => {},
334
- verbose: () => {},
335
- },
336
- DiagLogLevel.ERROR,
337
- );
338
- try {
339
- makeTracer(tracerConfig("svc-1", "http://collector-1/v1/traces"));
340
- makeTracer(tracerConfig("svc-2", "http://collector-2/v1/traces"));
341
- } finally {
342
- diag.disable();
343
- }
344
-
345
- expect(errors).toEqual([]);
346
- expect(warnSpy).toHaveBeenCalledTimes(1);
347
- });
348
- });
349
- });
@@ -1,190 +0,0 @@
1
- import {
2
- context,
3
- defaultTextMapSetter,
4
- ROOT_CONTEXT,
5
- SpanKind,
6
- trace,
7
- Tracer,
8
- } from "@opentelemetry/api";
9
- import type { Span as ApiSpan } from "@opentelemetry/api";
10
- import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
11
- import { W3CTraceContextPropagator } from "@opentelemetry/core";
12
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
13
- import { AWSXRayIdGenerator } from "@opentelemetry/id-generator-aws-xray";
14
- import {
15
- BatchSpanProcessor,
16
- type Span,
17
- TracerProvider,
18
- } from "@opentelemetry/sdk-trace";
19
- import {
20
- ATTR_HTTP_REQUEST_METHOD,
21
- ATTR_HTTP_ROUTE,
22
- } from "@opentelemetry/semantic-conventions";
23
- import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
24
- import { validateServiceIdentity } from "./utils/configValidation";
25
- import { createOTelResource } from "./utils/createResource";
26
-
27
- let globalTracerProvider: TracerProvider | undefined;
28
- let contextManagerInitialized = false;
29
-
30
- function getRegisteredTracerProvider(): unknown {
31
- const provider = trace.getTracerProvider() as unknown as {
32
- getDelegate?: () => unknown;
33
- };
34
- return typeof provider.getDelegate === "function"
35
- ? provider.getDelegate()
36
- : provider;
37
- }
38
-
39
- const ALREADY_REGISTERED_WARNING =
40
- "[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.";
41
-
42
- /**
43
- * Options accepted by {@link StandardTracer.startSpan}.
44
- */
45
- export interface StandardTracerStartSpanOptions {
46
- /**
47
- * OpenTelemetry span kind (default `SpanKind.INTERNAL`).
48
- *
49
- * Providing options means the caller describes the span itself: the
50
- * parentless default attributes (`http.request.method=BACKEND` and the
51
- * synthetic `http.route`) are not added.
52
- */
53
- kind?: SpanKind;
54
- }
55
-
56
- export class StandardTracer {
57
- private static readonly SPAN_NAME_SANITIZE_RE = /[^a-zA-Z0-9-_/]/g;
58
- private static readonly PROPAGATOR = new W3CTraceContextPropagator();
59
-
60
- private traceProvider: TracerProvider;
61
- private tracer: Tracer;
62
- private serviceVersion: string;
63
- private serviceName: string;
64
- private ownsGlobalProvider = false;
65
- private shutdownPromise?: Promise<void>;
66
-
67
- constructor(config: ConfigOTelInterface) {
68
- validateServiceIdentity(config);
69
- this.serviceName = config.SERVICE_ID;
70
- this.serviceVersion = config.VERSION;
71
- const spanProcessors = [];
72
-
73
- if (config.OPENTELEMETRY_COLLECTOR_HTTP_TRACES) {
74
- const exporterHeaders: Record<string, string> = {};
75
- if (config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER) {
76
- exporterHeaders["Authorization"] =
77
- `Bearer ${config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER}`;
78
- }
79
- const exporter = new OTLPTraceExporter({
80
- url: config.OPENTELEMETRY_COLLECTOR_HTTP_TRACES,
81
- headers: exporterHeaders,
82
- });
83
- spanProcessors.push(new BatchSpanProcessor({ exporter }));
84
- }
85
- this.traceProvider = new TracerProvider({
86
- idGenerator: new AWSXRayIdGenerator(),
87
- resource: createOTelResource(this.serviceName, this.serviceVersion),
88
- spanProcessors,
89
- });
90
- // Resolve the tracer from this instance's provider: the global API may be
91
- // owned by another instance and would silently export through it.
92
- this.tracer = this.traceProvider.getTracer(
93
- `${this.serviceName}:${this.serviceVersion}`,
94
- );
95
- this.registerGlobalProvider();
96
- initializeContextManager();
97
- }
98
-
99
- public shutdown(): Promise<void> {
100
- if (!this.shutdownPromise) {
101
- this.shutdownPromise = this.doShutdown();
102
- }
103
- return this.shutdownPromise;
104
- }
105
-
106
- public startSpan(
107
- name: string,
108
- parentSpan?: Span,
109
- options?: StandardTracerStartSpanOptions,
110
- ): Span {
111
- const sanitizedName = name.replace(
112
- StandardTracer.SPAN_NAME_SANITIZE_RE,
113
- "_",
114
- );
115
- if (parentSpan) {
116
- return this.tracer.startSpan(
117
- sanitizedName,
118
- options,
119
- trace.setSpan(context.active(), parentSpan),
120
- ) as Span;
121
- }
122
- if (options) {
123
- return this.tracer.startSpan(sanitizedName, options) as Span;
124
- }
125
- const span = this.tracer.startSpan(sanitizedName) as Span;
126
- span.setAttribute(ATTR_HTTP_REQUEST_METHOD, `BACKEND`);
127
- span.setAttribute(
128
- ATTR_HTTP_ROUTE,
129
- `${this.serviceName}-${this.serviceVersion}-${sanitizedName}`,
130
- );
131
- return span;
132
- }
133
-
134
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
- public static updateHttpHeader(context: ApiSpan, headers = {}): any {
136
- StandardTracer.PROPAGATOR.inject(
137
- trace.setSpanContext(ROOT_CONTEXT, context.spanContext()),
138
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
- headers as any,
140
- defaultTextMapSetter,
141
- );
142
- return headers;
143
- }
144
-
145
- private registerGlobalProvider(): void {
146
- if (globalTracerProvider) {
147
- if (getRegisteredTracerProvider() === globalTracerProvider) {
148
- console.warn(ALREADY_REGISTERED_WARNING);
149
- return;
150
- }
151
- // The previous provider is no longer registered (disabled elsewhere).
152
- globalTracerProvider = undefined;
153
- }
154
- if (trace.setGlobalTracerProvider(this.traceProvider)) {
155
- globalTracerProvider = this.traceProvider;
156
- this.ownsGlobalProvider = true;
157
- } else {
158
- console.warn(ALREADY_REGISTERED_WARNING);
159
- }
160
- }
161
-
162
- private async doShutdown(): Promise<void> {
163
- await this.traceProvider.shutdown();
164
- if (
165
- this.ownsGlobalProvider &&
166
- getRegisteredTracerProvider() === this.traceProvider
167
- ) {
168
- trace.disable();
169
- }
170
- if (globalTracerProvider === this.traceProvider) {
171
- globalTracerProvider = undefined;
172
- }
173
- this.ownsGlobalProvider = false;
174
- }
175
- }
176
-
177
- function initializeContextManager(): void {
178
- if (contextManagerInitialized) {
179
- return;
180
- }
181
- contextManagerInitialized = true;
182
- const contextManager = new AsyncLocalStorageContextManager();
183
- if (context.setGlobalContextManager(contextManager)) {
184
- contextManager.enable();
185
- } else {
186
- console.warn(
187
- "[otel-utils] A global context manager is already registered; StandardTracer will keep using it for context propagation.",
188
- );
189
- }
190
- }
@@ -1,20 +0,0 @@
1
- import { createOTelResource } from "./utils/createResource";
2
- import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_NETWORK_LOCAL_ADDRESS } from "@opentelemetry/semantic-conventions";
3
-
4
- describe("createOTelResource", () => {
5
- it("should create a resource with the provided service name and version", () => {
6
- const resource = createOTelResource("test-service", "1.0.0");
7
- const attrs = resource.attributes;
8
-
9
- expect(attrs[ATTR_SERVICE_NAME]).toBe("test-service");
10
- expect(attrs[ATTR_SERVICE_VERSION]).toBe("1.0.0");
11
- });
12
-
13
- it("should include the local hostname as network local address", () => {
14
- const resource = createOTelResource("test-service", "1.0.0");
15
- const attrs = resource.attributes;
16
-
17
- expect(attrs[ATTR_NETWORK_LOCAL_ADDRESS]).toBeDefined();
18
- expect(typeof attrs[ATTR_NETWORK_LOCAL_ADDRESS]).toBe("string");
19
- });
20
- });
@@ -1,16 +0,0 @@
1
- export type ConsoleLogMode = "always" | "fallback" | "never";
2
-
3
- export interface ConfigOTelInterface {
4
- SERVICE_ID: string;
5
- VERSION: string;
6
- OPENTELEMETRY_COLLECTOR_HTTP_TRACES?: string;
7
- OPENTELEMETRY_COLLECTOR_HTTP_METRICS?: string;
8
- OPENTELEMETRY_COLLECTOR_HTTP_LOGS?: string;
9
- OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS?: number;
10
- OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS?: number;
11
- OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE?: number;
12
- OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE?: number;
13
- OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS?: number;
14
- OPENTELEMETRY_CONSOLE_LOG_MODE?: ConsoleLogMode;
15
- OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER?: string;
16
- }
@@ -1,5 +0,0 @@
1
- import type { Logger } from "@opentelemetry/api";
2
-
3
- export interface StandardLoggerInterface {
4
- getLogger(): Logger | undefined;
5
- }