@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.
- package/README.md +36 -29
- package/dist/src/StandardLogger.js +3 -1
- package/dist/src/StandardTracer.d.ts +8 -2
- package/dist/src/StandardTracer.js +7 -4
- package/package.json +18 -5
- package/.github/workflows/main-build.yml +0 -16
- package/.github/workflows/pr-check.yml +0 -25
- package/index.ts +0 -7
- package/jest.config.js +0 -17
- package/prettierrc.json +0 -5
- package/src/ModuleLogger.spec.ts +0 -253
- package/src/ModuleLogger.ts +0 -137
- package/src/ShutdownHooks.spec.ts +0 -75
- package/src/ShutdownHooks.ts +0 -45
- package/src/StandardLogger.spec.ts +0 -479
- package/src/StandardLogger.ts +0 -149
- package/src/StandardMeter.spec.ts +0 -446
- package/src/StandardMeter.ts +0 -167
- package/src/StandardTracer.spec.ts +0 -349
- package/src/StandardTracer.ts +0 -190
- package/src/createResource.spec.ts +0 -20
- package/src/models/ConfigOTelInterface.ts +0 -16
- package/src/models/StandardLoggerInterface.ts +0 -5
- package/src/utils/configValidation.spec.ts +0 -141
- package/src/utils/configValidation.ts +0 -50
- package/src/utils/createResource.ts +0 -20
- package/tsconfig.json +0 -14
- package/tsconfig.spec.json +0 -8
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import { metrics } from "@opentelemetry/api";
|
|
2
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
3
|
-
import {
|
|
4
|
-
DataPointType,
|
|
5
|
-
type InMemoryMetricExporter,
|
|
6
|
-
MeterProvider,
|
|
7
|
-
type MetricData,
|
|
8
|
-
} from "@opentelemetry/sdk-metrics";
|
|
9
|
-
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
10
|
-
import { StandardMeter } from "./StandardMeter";
|
|
11
|
-
|
|
12
|
-
jest.mock("@opentelemetry/sdk-metrics", () => {
|
|
13
|
-
const actual =
|
|
14
|
-
jest.requireActual<typeof import("@opentelemetry/sdk-metrics")>(
|
|
15
|
-
"@opentelemetry/sdk-metrics",
|
|
16
|
-
);
|
|
17
|
-
const metricReaderOptions: unknown[] = [];
|
|
18
|
-
class RecordingPeriodicExportingMetricReader extends actual.PeriodicExportingMetricReader {
|
|
19
|
-
constructor(
|
|
20
|
-
options: ConstructorParameters<
|
|
21
|
-
typeof actual.PeriodicExportingMetricReader
|
|
22
|
-
>[0],
|
|
23
|
-
) {
|
|
24
|
-
super(options);
|
|
25
|
-
metricReaderOptions.push(options);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
...actual,
|
|
30
|
-
PeriodicExportingMetricReader: RecordingPeriodicExportingMetricReader,
|
|
31
|
-
__metricReaderOptions: metricReaderOptions,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
interface SdkMetricsMock {
|
|
36
|
-
__metricReaderOptions: unknown[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function metricReaderOptions(): unknown[] {
|
|
40
|
-
return (jest.requireMock("@opentelemetry/sdk-metrics") as SdkMetricsMock)
|
|
41
|
-
.__metricReaderOptions;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
jest.mock("@opentelemetry/exporter-metrics-otlp-http", () => {
|
|
45
|
-
const {
|
|
46
|
-
AggregationTemporality,
|
|
47
|
-
InMemoryMetricExporter,
|
|
48
|
-
} = jest.requireActual<typeof import("@opentelemetry/sdk-metrics")>(
|
|
49
|
-
"@opentelemetry/sdk-metrics",
|
|
50
|
-
);
|
|
51
|
-
return {
|
|
52
|
-
OTLPMetricExporter: jest.fn().mockImplementation(function () {
|
|
53
|
-
return new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
|
|
54
|
-
}),
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const mockExporterConstructor = OTLPMetricExporter as unknown as jest.Mock;
|
|
59
|
-
|
|
60
|
-
function exporterAt(index: number): InMemoryMetricExporter {
|
|
61
|
-
return mockExporterConstructor.mock.results[index]
|
|
62
|
-
.value as InMemoryMetricExporter;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function lastExporter(): InMemoryMetricExporter {
|
|
66
|
-
return exporterAt(mockExporterConstructor.mock.results.length - 1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function metricNames(exporter: InMemoryMetricExporter): string[] {
|
|
70
|
-
return exporter
|
|
71
|
-
.getMetrics()
|
|
72
|
-
.flatMap((resourceMetrics) => resourceMetrics.scopeMetrics)
|
|
73
|
-
.flatMap((scopeMetrics) => scopeMetrics.metrics)
|
|
74
|
-
.map((metric) => metric.descriptor.name);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function metricData(
|
|
78
|
-
exporter: InMemoryMetricExporter,
|
|
79
|
-
name: string,
|
|
80
|
-
): MetricData | undefined {
|
|
81
|
-
return exporter
|
|
82
|
-
.getMetrics()
|
|
83
|
-
.flatMap((resourceMetrics) => resourceMetrics.scopeMetrics)
|
|
84
|
-
.flatMap((scopeMetrics) => scopeMetrics.metrics)
|
|
85
|
-
.find((metric) => metric.descriptor.name === name);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const SEMCONV_HTTP_DURATION_BOUNDARIES = [
|
|
89
|
-
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
function meterConfig(
|
|
93
|
-
serviceId: string,
|
|
94
|
-
url?: string,
|
|
95
|
-
): ConfigOTelInterface {
|
|
96
|
-
return {
|
|
97
|
-
SERVICE_ID: serviceId,
|
|
98
|
-
VERSION: "1.0.0",
|
|
99
|
-
OPENTELEMETRY_COLLECTOR_HTTP_METRICS: url,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
describe("StandardMeter", () => {
|
|
104
|
-
const createdMeters: StandardMeter[] = [];
|
|
105
|
-
let warnSpy: jest.SpyInstance;
|
|
106
|
-
|
|
107
|
-
function makeMeter(config: ConfigOTelInterface): StandardMeter {
|
|
108
|
-
const meter = new StandardMeter(config);
|
|
109
|
-
createdMeters.push(meter);
|
|
110
|
-
return meter;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
jest.clearAllMocks();
|
|
115
|
-
metricReaderOptions().length = 0;
|
|
116
|
-
metrics.disable();
|
|
117
|
-
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
afterEach(async () => {
|
|
121
|
-
for (const meter of createdMeters.splice(0)) {
|
|
122
|
-
await meter.shutdown();
|
|
123
|
-
}
|
|
124
|
-
metrics.disable();
|
|
125
|
-
warnSpy.mockRestore();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("builds the metric exporter with URL and concurrency limit and honors the export interval", async () => {
|
|
129
|
-
const meter = makeMeter({
|
|
130
|
-
SERVICE_ID: "metric-service",
|
|
131
|
-
VERSION: "1.0.0",
|
|
132
|
-
OPENTELEMETRY_COLLECTOR_HTTP_METRICS: "http://collector:4318/v1/metrics",
|
|
133
|
-
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS: 5,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(mockExporterConstructor).toHaveBeenCalledWith({
|
|
137
|
-
url: "http://collector:4318/v1/metrics",
|
|
138
|
-
headers: {},
|
|
139
|
-
concurrencyLimit: 5,
|
|
140
|
-
});
|
|
141
|
-
await meter.shutdown();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("adds the Authorization header when configured", async () => {
|
|
145
|
-
const meter = makeMeter({
|
|
146
|
-
SERVICE_ID: "metric-service",
|
|
147
|
-
VERSION: "1.0.0",
|
|
148
|
-
OPENTELEMETRY_COLLECTOR_HTTP_METRICS: "http://collector:4318/v1/metrics",
|
|
149
|
-
OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER: "secret",
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(mockExporterConstructor).toHaveBeenCalledWith({
|
|
153
|
-
url: "http://collector:4318/v1/metrics",
|
|
154
|
-
headers: { Authorization: "Bearer secret" },
|
|
155
|
-
concurrencyLimit: 5,
|
|
156
|
-
});
|
|
157
|
-
await meter.shutdown();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("creates the four metric types and flushes them on shutdown", async () => {
|
|
161
|
-
const meter = makeMeter(
|
|
162
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
meter.createCounter("requests.total").add(1);
|
|
166
|
-
meter.createUpDownCounter("users.active").add(1);
|
|
167
|
-
meter.createHistogram("requests.latency").record(42);
|
|
168
|
-
const gauge = meter.createObservableGauge(
|
|
169
|
-
"cpu.usage",
|
|
170
|
-
(observableResult) => observableResult.observe(0.5),
|
|
171
|
-
"Current CPU usage",
|
|
172
|
-
);
|
|
173
|
-
expect(gauge).toBeDefined();
|
|
174
|
-
|
|
175
|
-
await meter.shutdown();
|
|
176
|
-
|
|
177
|
-
const exporter = lastExporter();
|
|
178
|
-
expect(metricNames(exporter)).toEqual(
|
|
179
|
-
expect.arrayContaining([
|
|
180
|
-
"metric-service.requests.total",
|
|
181
|
-
"metric-service.users.active",
|
|
182
|
-
"metric-service.requests.latency",
|
|
183
|
-
"cpu.usage",
|
|
184
|
-
]),
|
|
185
|
-
);
|
|
186
|
-
expect(metricData(exporter, "cpu.usage")?.descriptor.description).toBe(
|
|
187
|
-
"Current CPU usage",
|
|
188
|
-
);
|
|
189
|
-
const counter = metricData(exporter, "metric-service.requests.total");
|
|
190
|
-
expect(counter?.descriptor.unit).toBe("");
|
|
191
|
-
expect(counter?.descriptor.description).toBe("");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("records histogram values into the configured semconv seconds buckets", async () => {
|
|
195
|
-
const meter = makeMeter(
|
|
196
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const histogram = meter.createHistogram("http.server.request.duration", {
|
|
200
|
-
description: "Duration of HTTP server requests",
|
|
201
|
-
unit: "s",
|
|
202
|
-
advice: { explicitBucketBoundaries: SEMCONV_HTTP_DURATION_BOUNDARIES },
|
|
203
|
-
});
|
|
204
|
-
for (const seconds of [0.005, 0.042, 0.08, 0.3, 1.2, 4.5]) {
|
|
205
|
-
histogram.record(seconds);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
await meter.shutdown();
|
|
209
|
-
|
|
210
|
-
const metric = metricData(
|
|
211
|
-
lastExporter(),
|
|
212
|
-
"metric-service.http.server.request.duration",
|
|
213
|
-
);
|
|
214
|
-
expect(metric).toBeDefined();
|
|
215
|
-
expect(metric?.descriptor.description).toBe(
|
|
216
|
-
"Duration of HTTP server requests",
|
|
217
|
-
);
|
|
218
|
-
expect(metric?.descriptor.unit).toBe("s");
|
|
219
|
-
if (!metric || metric.dataPointType !== DataPointType.HISTOGRAM) {
|
|
220
|
-
throw new Error("expected a histogram metric");
|
|
221
|
-
}
|
|
222
|
-
const [dataPoint] = metric.dataPoints;
|
|
223
|
-
expect(dataPoint.value.count).toBe(6);
|
|
224
|
-
expect(dataPoint.value.buckets.boundaries).toEqual(
|
|
225
|
-
SEMCONV_HTTP_DURATION_BOUNDARIES,
|
|
226
|
-
);
|
|
227
|
-
expect(dataPoint.value.buckets.counts).toEqual([
|
|
228
|
-
1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0,
|
|
229
|
-
]);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("forwards description and unit options for counters and up-down counters", async () => {
|
|
233
|
-
const meter = makeMeter(
|
|
234
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
meter
|
|
238
|
-
.createCounter("requests.total", {
|
|
239
|
-
description: "Total requests",
|
|
240
|
-
unit: "1",
|
|
241
|
-
})
|
|
242
|
-
.add(1);
|
|
243
|
-
meter
|
|
244
|
-
.createUpDownCounter("users.active", {
|
|
245
|
-
description: "Active users",
|
|
246
|
-
unit: "{user}",
|
|
247
|
-
})
|
|
248
|
-
.add(1);
|
|
249
|
-
|
|
250
|
-
await meter.shutdown();
|
|
251
|
-
|
|
252
|
-
const exporter = lastExporter();
|
|
253
|
-
const counter = metricData(exporter, "metric-service.requests.total");
|
|
254
|
-
expect(counter?.descriptor.description).toBe("Total requests");
|
|
255
|
-
expect(counter?.descriptor.unit).toBe("1");
|
|
256
|
-
const upDownCounter = metricData(exporter, "metric-service.users.active");
|
|
257
|
-
expect(upDownCounter?.descriptor.description).toBe("Active users");
|
|
258
|
-
expect(upDownCounter?.descriptor.unit).toBe("{user}");
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("accepts an options object for observable gauges and keeps them unprefixed", async () => {
|
|
262
|
-
const meter = makeMeter(
|
|
263
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
meter.createObservableGauge(
|
|
267
|
-
"cpu.usage",
|
|
268
|
-
(observableResult) => observableResult.observe(0.5),
|
|
269
|
-
{ description: "Current CPU usage", unit: "1" },
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
await meter.shutdown();
|
|
273
|
-
|
|
274
|
-
const exporter = lastExporter();
|
|
275
|
-
const names = metricNames(exporter);
|
|
276
|
-
expect(names).toContain("cpu.usage");
|
|
277
|
-
expect(names).not.toContain("metric-service.cpu.usage");
|
|
278
|
-
const gauge = metricData(exporter, "cpu.usage");
|
|
279
|
-
expect(gauge?.descriptor.description).toBe("Current CPU usage");
|
|
280
|
-
expect(gauge?.descriptor.unit).toBe("1");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("creates a histogram without the service-name prefix when the unprefixed option is set", async () => {
|
|
284
|
-
const meter = makeMeter(
|
|
285
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
meter.createHistogram("session.duration", { unprefixed: true }).record(1);
|
|
289
|
-
meter.createHistogram("requests.latency").record(42);
|
|
290
|
-
|
|
291
|
-
await meter.shutdown();
|
|
292
|
-
|
|
293
|
-
const names = metricNames(lastExporter());
|
|
294
|
-
expect(names).toContain("session.duration");
|
|
295
|
-
expect(names).toContain("metric-service.requests.latency");
|
|
296
|
-
expect(names).not.toContain("metric-service.session.duration");
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("works without a collector, leaves the global API untouched and without a gauge description", async () => {
|
|
300
|
-
const meter = makeMeter(meterConfig("metric-service"));
|
|
301
|
-
|
|
302
|
-
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
303
|
-
expect(metrics.getMeterProvider()).not.toBeInstanceOf(MeterProvider);
|
|
304
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
305
|
-
|
|
306
|
-
meter.createCounter("requests.total").add(1);
|
|
307
|
-
const gauge = meter.createObservableGauge("cpu.usage", (observableResult) =>
|
|
308
|
-
observableResult.observe(0.5),
|
|
309
|
-
);
|
|
310
|
-
expect(gauge).toBeDefined();
|
|
311
|
-
await expect(meter.shutdown()).resolves.toBeUndefined();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("registers a configured meter as the global provider", async () => {
|
|
315
|
-
const meter = makeMeter(
|
|
316
|
-
meterConfig("metric-service", "http://collector-1/v1/metrics"),
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
expect(metrics.getMeterProvider()).toBeInstanceOf(MeterProvider);
|
|
320
|
-
metrics.getMeter("global").createCounter("global.counter").add(1);
|
|
321
|
-
|
|
322
|
-
await meter.shutdown();
|
|
323
|
-
|
|
324
|
-
expect(metricNames(exporterAt(0))).toContain("global.counter");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("keeps each instance on its own exporter and warns on the second registration", async () => {
|
|
328
|
-
const first = makeMeter(
|
|
329
|
-
meterConfig("metric-a", "http://collector-1/v1/metrics"),
|
|
330
|
-
);
|
|
331
|
-
const second = makeMeter(
|
|
332
|
-
meterConfig("metric-b", "http://collector-2/v1/metrics"),
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
first.createCounter("a.total").add(1);
|
|
336
|
-
second.createCounter("b.total").add(1);
|
|
337
|
-
metrics.getMeter("global").createCounter("global.counter").add(1);
|
|
338
|
-
|
|
339
|
-
await first.shutdown();
|
|
340
|
-
await second.shutdown();
|
|
341
|
-
|
|
342
|
-
expect(metricNames(exporterAt(0))).toEqual(
|
|
343
|
-
expect.arrayContaining(["metric-a.a.total", "global.counter"]),
|
|
344
|
-
);
|
|
345
|
-
expect(metricNames(exporterAt(1))).toEqual(["metric-b.b.total"]);
|
|
346
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
347
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
348
|
-
expect.stringContaining("A global meter provider is already registered"),
|
|
349
|
-
);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("owner shutdown releases the global provider so a new meter can register", async () => {
|
|
353
|
-
const first = makeMeter(
|
|
354
|
-
meterConfig("metric-a", "http://collector-1/v1/metrics"),
|
|
355
|
-
);
|
|
356
|
-
expect(metrics.getMeterProvider()).toBeInstanceOf(MeterProvider);
|
|
357
|
-
|
|
358
|
-
await first.shutdown();
|
|
359
|
-
expect(metrics.getMeterProvider()).not.toBeInstanceOf(MeterProvider);
|
|
360
|
-
|
|
361
|
-
const second = makeMeter(
|
|
362
|
-
meterConfig("metric-b", "http://collector-2/v1/metrics"),
|
|
363
|
-
);
|
|
364
|
-
expect(metrics.getMeterProvider()).toBeInstanceOf(MeterProvider);
|
|
365
|
-
const shutdownSpy = jest.spyOn(exporterAt(1), "shutdown");
|
|
366
|
-
|
|
367
|
-
metrics.getMeter("global").createCounter("after-reinit").add(1);
|
|
368
|
-
await second.shutdown();
|
|
369
|
-
await second.shutdown();
|
|
370
|
-
|
|
371
|
-
expect(metricNames(exporterAt(1))).toContain("after-reinit");
|
|
372
|
-
expect(shutdownSpy).toHaveBeenCalledTimes(1);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe("export interval tuning", () => {
|
|
376
|
-
it("uses the documented default interval", async () => {
|
|
377
|
-
const meter = makeMeter(
|
|
378
|
-
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
expect(metricReaderOptions()).toHaveLength(1);
|
|
382
|
-
expect(metricReaderOptions()[0]).toEqual({
|
|
383
|
-
exporter: expect.anything(),
|
|
384
|
-
exportIntervalMillis: 60000,
|
|
385
|
-
});
|
|
386
|
-
await meter.shutdown();
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it.each([0, -5, 0.5])(
|
|
390
|
-
"clamps the export interval of %p seconds to one second",
|
|
391
|
-
async (interval) => {
|
|
392
|
-
const meter = makeMeter({
|
|
393
|
-
...meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
394
|
-
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS: interval,
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
expect(
|
|
398
|
-
(metricReaderOptions()[0] as { exportIntervalMillis: number })
|
|
399
|
-
.exportIntervalMillis,
|
|
400
|
-
).toBe(1000);
|
|
401
|
-
await meter.shutdown();
|
|
402
|
-
},
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
it("falls back to the default interval for a NaN value", async () => {
|
|
406
|
-
const meter = makeMeter({
|
|
407
|
-
...meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
408
|
-
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS: NaN,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
expect(
|
|
412
|
-
(metricReaderOptions()[0] as { exportIntervalMillis: number })
|
|
413
|
-
.exportIntervalMillis,
|
|
414
|
-
).toBe(60000);
|
|
415
|
-
await meter.shutdown();
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
describe("service identity validation", () => {
|
|
420
|
-
it("rejects an empty SERVICE_ID without creating an exporter", () => {
|
|
421
|
-
expect(
|
|
422
|
-
() =>
|
|
423
|
-
new StandardMeter({
|
|
424
|
-
SERVICE_ID: " ",
|
|
425
|
-
VERSION: "1.0.0",
|
|
426
|
-
OPENTELEMETRY_COLLECTOR_HTTP_METRICS:
|
|
427
|
-
"http://collector:4318/v1/metrics",
|
|
428
|
-
}),
|
|
429
|
-
).toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
|
|
430
|
-
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
it("rejects an empty VERSION without creating an exporter", () => {
|
|
434
|
-
expect(
|
|
435
|
-
() =>
|
|
436
|
-
new StandardMeter({
|
|
437
|
-
SERVICE_ID: "metric-service",
|
|
438
|
-
VERSION: "",
|
|
439
|
-
OPENTELEMETRY_COLLECTOR_HTTP_METRICS:
|
|
440
|
-
"http://collector:4318/v1/metrics",
|
|
441
|
-
}),
|
|
442
|
-
).toThrow("[otel-utils] VERSION must be a non-empty string");
|
|
443
|
-
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
});
|
package/src/StandardMeter.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Counter,
|
|
3
|
-
Histogram,
|
|
4
|
-
Meter,
|
|
5
|
-
metrics,
|
|
6
|
-
type MetricOptions,
|
|
7
|
-
ObservableGauge,
|
|
8
|
-
UpDownCounter,
|
|
9
|
-
} from "@opentelemetry/api";
|
|
10
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
11
|
-
import { createOTelResource } from "./utils/createResource";
|
|
12
|
-
import {
|
|
13
|
-
MeterProvider,
|
|
14
|
-
PeriodicExportingMetricReader,
|
|
15
|
-
} from "@opentelemetry/sdk-metrics";
|
|
16
|
-
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
17
|
-
import {
|
|
18
|
-
normalizeExportIntervalSeconds,
|
|
19
|
-
validateServiceIdentity,
|
|
20
|
-
} from "./utils/configValidation";
|
|
21
|
-
|
|
22
|
-
let globalMeterProvider: MeterProvider | undefined;
|
|
23
|
-
|
|
24
|
-
const ALREADY_REGISTERED_WARNING =
|
|
25
|
-
"[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.";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options accepted by {@link StandardMeter.createHistogram}.
|
|
29
|
-
* `description`, `unit`, `valueType` and `advice` are forwarded to the
|
|
30
|
-
* OpenTelemetry Meter; `advice.explicitBucketBoundaries` sets the histogram
|
|
31
|
-
* bucket boundaries.
|
|
32
|
-
*/
|
|
33
|
-
export interface StandardMeterCreateHistogramOptions extends MetricOptions {
|
|
34
|
-
/**
|
|
35
|
-
* Create the histogram without the `${serviceName}.` prefix:
|
|
36
|
-
* exported exactly as `key`.
|
|
37
|
-
*/
|
|
38
|
-
unprefixed?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class StandardMeter {
|
|
42
|
-
private meter: Meter;
|
|
43
|
-
private meterProvider: MeterProvider;
|
|
44
|
-
private serviceVersion: string;
|
|
45
|
-
private serviceName: string;
|
|
46
|
-
private ownsGlobalProvider = false;
|
|
47
|
-
private shutdownPromise?: Promise<void>;
|
|
48
|
-
|
|
49
|
-
constructor(config: ConfigOTelInterface) {
|
|
50
|
-
validateServiceIdentity(config);
|
|
51
|
-
this.serviceName = config.SERVICE_ID;
|
|
52
|
-
this.serviceVersion = config.VERSION;
|
|
53
|
-
let hasCollector = false;
|
|
54
|
-
if (config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS) {
|
|
55
|
-
hasCollector = true;
|
|
56
|
-
const collectorOptions = {
|
|
57
|
-
url: config.OPENTELEMETRY_COLLECTOR_HTTP_METRICS,
|
|
58
|
-
headers: {} as Record<string, string>,
|
|
59
|
-
concurrencyLimit: 5,
|
|
60
|
-
};
|
|
61
|
-
if (config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER) {
|
|
62
|
-
collectorOptions.headers["Authorization"] =
|
|
63
|
-
`Bearer ${config.OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER}`;
|
|
64
|
-
}
|
|
65
|
-
const metricExporter = new OTLPMetricExporter(collectorOptions);
|
|
66
|
-
this.meterProvider = new MeterProvider({
|
|
67
|
-
resource: createOTelResource(this.serviceName, this.serviceVersion),
|
|
68
|
-
readers: [
|
|
69
|
-
new PeriodicExportingMetricReader({
|
|
70
|
-
exporter: metricExporter,
|
|
71
|
-
exportIntervalMillis:
|
|
72
|
-
normalizeExportIntervalSeconds(
|
|
73
|
-
config.OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS,
|
|
74
|
-
60,
|
|
75
|
-
) * 1000,
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
});
|
|
79
|
-
} else {
|
|
80
|
-
this.meterProvider = new MeterProvider({
|
|
81
|
-
resource: createOTelResource(this.serviceName, this.serviceVersion),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
this.meter = this.meterProvider.getMeter(
|
|
85
|
-
`${this.serviceName}:${this.serviceVersion}`,
|
|
86
|
-
);
|
|
87
|
-
if (hasCollector) {
|
|
88
|
-
this.registerGlobalProvider();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
public shutdown(): Promise<void> {
|
|
93
|
-
if (!this.shutdownPromise) {
|
|
94
|
-
this.shutdownPromise = this.doShutdown();
|
|
95
|
-
}
|
|
96
|
-
return this.shutdownPromise;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
public createCounter(key: string, options?: MetricOptions): Counter {
|
|
100
|
-
return this.meter.createCounter(`${this.serviceName}.${key}`, options);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public createUpDownCounter(
|
|
104
|
-
key: string,
|
|
105
|
-
options?: MetricOptions,
|
|
106
|
-
): UpDownCounter {
|
|
107
|
-
return this.meter.createUpDownCounter(`${this.serviceName}.${key}`, options);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public createHistogram(
|
|
111
|
-
key: string,
|
|
112
|
-
options?: StandardMeterCreateHistogramOptions,
|
|
113
|
-
): Histogram {
|
|
114
|
-
const { unprefixed, ...instrumentOptions } = options ?? {};
|
|
115
|
-
return this.meter.createHistogram(
|
|
116
|
-
unprefixed ? key : `${this.serviceName}.${key}`,
|
|
117
|
-
instrumentOptions,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public createObservableGauge(
|
|
122
|
-
key: string,
|
|
123
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
-
callback: (observableResult: any) => void,
|
|
125
|
-
descriptionOrOptions?: string | MetricOptions,
|
|
126
|
-
): ObservableGauge {
|
|
127
|
-
const observableGauge = this.meter.createObservableGauge(
|
|
128
|
-
key,
|
|
129
|
-
typeof descriptionOrOptions === "string"
|
|
130
|
-
? { description: descriptionOrOptions }
|
|
131
|
-
: descriptionOrOptions,
|
|
132
|
-
);
|
|
133
|
-
observableGauge.addCallback(callback);
|
|
134
|
-
return observableGauge;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private registerGlobalProvider(): void {
|
|
138
|
-
if (globalMeterProvider) {
|
|
139
|
-
if (globalMeterProvider === metrics.getMeterProvider()) {
|
|
140
|
-
console.warn(ALREADY_REGISTERED_WARNING);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// The previous provider is no longer registered (disabled elsewhere).
|
|
144
|
-
globalMeterProvider = undefined;
|
|
145
|
-
}
|
|
146
|
-
if (metrics.setGlobalMeterProvider(this.meterProvider)) {
|
|
147
|
-
globalMeterProvider = this.meterProvider;
|
|
148
|
-
this.ownsGlobalProvider = true;
|
|
149
|
-
} else {
|
|
150
|
-
console.warn(ALREADY_REGISTERED_WARNING);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private async doShutdown(): Promise<void> {
|
|
155
|
-
await this.meterProvider.shutdown();
|
|
156
|
-
if (
|
|
157
|
-
this.ownsGlobalProvider &&
|
|
158
|
-
metrics.getMeterProvider() === this.meterProvider
|
|
159
|
-
) {
|
|
160
|
-
metrics.disable();
|
|
161
|
-
}
|
|
162
|
-
if (globalMeterProvider === this.meterProvider) {
|
|
163
|
-
globalMeterProvider = undefined;
|
|
164
|
-
}
|
|
165
|
-
this.ownsGlobalProvider = false;
|
|
166
|
-
}
|
|
167
|
-
}
|