@devopsplaybook.io/otel-utils 1.6.0 → 1.7.0-beta.38.e32e141
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 +47 -4
- package/dist/src/StandardMeter.d.ts +8 -5
- package/dist/src/StandardMeter.js +10 -7
- package/package.json +1 -1
- package/src/StandardMeter.spec.ts +114 -1
- package/src/StandardMeter.ts +19 -8
package/README.md
CHANGED
|
@@ -119,12 +119,55 @@ const cpuGauge = meter.createObservableGauge(
|
|
|
119
119
|
);
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
**Instrument options**
|
|
123
|
+
|
|
124
|
+
All four factories accept an optional options argument with the standard OpenTelemetry `MetricOptions` — `description`, `unit`, `valueType` and `advice` — forwarded to the underlying `Meter`. `createObservableGauge` also keeps accepting a description string as its third argument.
|
|
125
|
+
|
|
126
|
+
These options matter most for histograms: the SDK default bucket boundaries (`[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]`) are millisecond-oriented. A consumer that records seconds with the defaults puts every value below 5 s into the first bucket, which makes percentiles meaningless. The OpenTelemetry semantic conventions for `http.server.request.duration` therefore expect seconds and these boundaries:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const httpDuration = meter.createHistogram("http.server.request.duration", {
|
|
130
|
+
description: "Duration of HTTP server requests",
|
|
131
|
+
unit: "s",
|
|
132
|
+
advice: {
|
|
133
|
+
explicitBucketBoundaries: [
|
|
134
|
+
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,
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
httpDuration.record(Number(process.hrtime.bigint() - startTime) / 1e9);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The other factories accept the same options:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
meter.createCounter("requests.total", {
|
|
146
|
+
description: "Total requests",
|
|
147
|
+
unit: "1",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
meter.createObservableGauge(
|
|
151
|
+
"cpu.usage",
|
|
152
|
+
(result) => result.observe(cpuPercent),
|
|
153
|
+
{ description: "Current CPU usage", unit: "1" },
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Metric naming**
|
|
158
|
+
|
|
159
|
+
- Counters, up-down counters and histograms are exported as `${serviceName}.${key}`.
|
|
160
|
+
- A histogram created with `{ unprefixed: true }` is exported exactly as `key`; only histograms have this opt-out.
|
|
161
|
+
- Observable gauges are always exported exactly as `key` (never prefixed), including when an options object is passed.
|
|
162
|
+
|
|
163
|
+
These defaults are unchanged: existing metric names keep working.
|
|
164
|
+
|
|
122
165
|
**Methods:**
|
|
123
166
|
|
|
124
|
-
- `createCounter(key)` — Creates a `Counter`
|
|
125
|
-
- `createUpDownCounter(key)` — Creates an `UpDownCounter`.
|
|
126
|
-
- `createHistogram(key, options?)` — Creates a `Histogram` named `${serviceName}.${key}`; with `{ unprefixed: true }` the histogram is created exactly as `key`, without the service-name prefix.
|
|
127
|
-
- `createObservableGauge(key, callback,
|
|
167
|
+
- `createCounter(key, options?)` — Creates a `Counter` named `${serviceName}.${key}`.
|
|
168
|
+
- `createUpDownCounter(key, options?)` — Creates an `UpDownCounter` named `${serviceName}.${key}`.
|
|
169
|
+
- `createHistogram(key, options?)` — Creates a `Histogram` named `${serviceName}.${key}`; with `{ unprefixed: true }` the histogram is created exactly as `key`, without the service-name prefix. Options also accept the OTel instrument options (`description`, `unit`, `valueType`, `advice`).
|
|
170
|
+
- `createObservableGauge(key, callback, descriptionOrOptions?)` — Creates an `ObservableGauge` named `key` with a callback. The third argument is an optional description string or an options object (`description`, `unit`, `valueType`, `advice`).
|
|
128
171
|
- `shutdown()` — Flushes buffered metrics and shuts the meter provider down. Idempotent.
|
|
129
172
|
|
|
130
173
|
## Provider lifecycle
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { Counter, Histogram, ObservableGauge, UpDownCounter } from "@opentelemetry/api";
|
|
1
|
+
import { Counter, Histogram, type MetricOptions, ObservableGauge, UpDownCounter } from "@opentelemetry/api";
|
|
2
2
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
3
3
|
/**
|
|
4
4
|
* Options accepted by {@link StandardMeter.createHistogram}.
|
|
5
|
+
* `description`, `unit`, `valueType` and `advice` are forwarded to the
|
|
6
|
+
* OpenTelemetry Meter; `advice.explicitBucketBoundaries` sets the histogram
|
|
7
|
+
* bucket boundaries.
|
|
5
8
|
*/
|
|
6
|
-
export interface StandardMeterCreateHistogramOptions {
|
|
9
|
+
export interface StandardMeterCreateHistogramOptions extends MetricOptions {
|
|
7
10
|
/**
|
|
8
11
|
* Create the histogram without the `${serviceName}.` prefix:
|
|
9
12
|
* exported exactly as `key`.
|
|
@@ -19,10 +22,10 @@ export declare class StandardMeter {
|
|
|
19
22
|
private shutdownPromise?;
|
|
20
23
|
constructor(config: ConfigOTelInterface);
|
|
21
24
|
shutdown(): Promise<void>;
|
|
22
|
-
createCounter(key: string): Counter;
|
|
23
|
-
createUpDownCounter(key: string): UpDownCounter;
|
|
25
|
+
createCounter(key: string, options?: MetricOptions): Counter;
|
|
26
|
+
createUpDownCounter(key: string, options?: MetricOptions): UpDownCounter;
|
|
24
27
|
createHistogram(key: string, options?: StandardMeterCreateHistogramOptions): Histogram;
|
|
25
|
-
createObservableGauge(key: string, callback: (observableResult: any) => void,
|
|
28
|
+
createObservableGauge(key: string, callback: (observableResult: any) => void, descriptionOrOptions?: string | MetricOptions): ObservableGauge;
|
|
26
29
|
private registerGlobalProvider;
|
|
27
30
|
private doShutdown;
|
|
28
31
|
}
|
|
@@ -52,19 +52,22 @@ class StandardMeter {
|
|
|
52
52
|
}
|
|
53
53
|
return this.shutdownPromise;
|
|
54
54
|
}
|
|
55
|
-
createCounter(key) {
|
|
56
|
-
return this.meter.createCounter(`${this.serviceName}.${key}
|
|
55
|
+
createCounter(key, options) {
|
|
56
|
+
return this.meter.createCounter(`${this.serviceName}.${key}`, options);
|
|
57
57
|
}
|
|
58
|
-
createUpDownCounter(key) {
|
|
59
|
-
return this.meter.createUpDownCounter(`${this.serviceName}.${key}
|
|
58
|
+
createUpDownCounter(key, options) {
|
|
59
|
+
return this.meter.createUpDownCounter(`${this.serviceName}.${key}`, options);
|
|
60
60
|
}
|
|
61
61
|
createHistogram(key, options) {
|
|
62
|
-
|
|
62
|
+
const { unprefixed, ...instrumentOptions } = options !== null && options !== void 0 ? options : {};
|
|
63
|
+
return this.meter.createHistogram(unprefixed ? key : `${this.serviceName}.${key}`, instrumentOptions);
|
|
63
64
|
}
|
|
64
65
|
createObservableGauge(key,
|
|
65
66
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
-
callback,
|
|
67
|
-
const observableGauge = this.meter.createObservableGauge(key,
|
|
67
|
+
callback, descriptionOrOptions) {
|
|
68
|
+
const observableGauge = this.meter.createObservableGauge(key, typeof descriptionOrOptions === "string"
|
|
69
|
+
? { description: descriptionOrOptions }
|
|
70
|
+
: descriptionOrOptions);
|
|
68
71
|
observableGauge.addCallback(callback);
|
|
69
72
|
return observableGauge;
|
|
70
73
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { metrics } from "@opentelemetry/api";
|
|
2
2
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
3
3
|
import {
|
|
4
|
+
DataPointType,
|
|
4
5
|
type InMemoryMetricExporter,
|
|
5
6
|
MeterProvider,
|
|
7
|
+
type MetricData,
|
|
6
8
|
} from "@opentelemetry/sdk-metrics";
|
|
7
9
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
8
10
|
import { StandardMeter } from "./StandardMeter";
|
|
@@ -40,6 +42,21 @@ function metricNames(exporter: InMemoryMetricExporter): string[] {
|
|
|
40
42
|
.map((metric) => metric.descriptor.name);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function metricData(
|
|
46
|
+
exporter: InMemoryMetricExporter,
|
|
47
|
+
name: string,
|
|
48
|
+
): MetricData | undefined {
|
|
49
|
+
return exporter
|
|
50
|
+
.getMetrics()
|
|
51
|
+
.flatMap((resourceMetrics) => resourceMetrics.scopeMetrics)
|
|
52
|
+
.flatMap((scopeMetrics) => scopeMetrics.metrics)
|
|
53
|
+
.find((metric) => metric.descriptor.name === name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SEMCONV_HTTP_DURATION_BOUNDARIES = [
|
|
57
|
+
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,
|
|
58
|
+
];
|
|
59
|
+
|
|
43
60
|
function meterConfig(
|
|
44
61
|
serviceId: string,
|
|
45
62
|
url?: string,
|
|
@@ -124,7 +141,8 @@ describe("StandardMeter", () => {
|
|
|
124
141
|
|
|
125
142
|
await meter.shutdown();
|
|
126
143
|
|
|
127
|
-
|
|
144
|
+
const exporter = lastExporter();
|
|
145
|
+
expect(metricNames(exporter)).toEqual(
|
|
128
146
|
expect.arrayContaining([
|
|
129
147
|
"metric-service.requests.total",
|
|
130
148
|
"metric-service.users.active",
|
|
@@ -132,6 +150,101 @@ describe("StandardMeter", () => {
|
|
|
132
150
|
"cpu.usage",
|
|
133
151
|
]),
|
|
134
152
|
);
|
|
153
|
+
expect(metricData(exporter, "cpu.usage")?.descriptor.description).toBe(
|
|
154
|
+
"Current CPU usage",
|
|
155
|
+
);
|
|
156
|
+
const counter = metricData(exporter, "metric-service.requests.total");
|
|
157
|
+
expect(counter?.descriptor.unit).toBe("");
|
|
158
|
+
expect(counter?.descriptor.description).toBe("");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("records histogram values into the configured semconv seconds buckets", async () => {
|
|
162
|
+
const meter = makeMeter(
|
|
163
|
+
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const histogram = meter.createHistogram("http.server.request.duration", {
|
|
167
|
+
description: "Duration of HTTP server requests",
|
|
168
|
+
unit: "s",
|
|
169
|
+
advice: { explicitBucketBoundaries: SEMCONV_HTTP_DURATION_BOUNDARIES },
|
|
170
|
+
});
|
|
171
|
+
for (const seconds of [0.005, 0.042, 0.08, 0.3, 1.2, 4.5]) {
|
|
172
|
+
histogram.record(seconds);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await meter.shutdown();
|
|
176
|
+
|
|
177
|
+
const metric = metricData(
|
|
178
|
+
lastExporter(),
|
|
179
|
+
"metric-service.http.server.request.duration",
|
|
180
|
+
);
|
|
181
|
+
expect(metric).toBeDefined();
|
|
182
|
+
expect(metric?.descriptor.description).toBe(
|
|
183
|
+
"Duration of HTTP server requests",
|
|
184
|
+
);
|
|
185
|
+
expect(metric?.descriptor.unit).toBe("s");
|
|
186
|
+
if (!metric || metric.dataPointType !== DataPointType.HISTOGRAM) {
|
|
187
|
+
throw new Error("expected a histogram metric");
|
|
188
|
+
}
|
|
189
|
+
const [dataPoint] = metric.dataPoints;
|
|
190
|
+
expect(dataPoint.value.count).toBe(6);
|
|
191
|
+
expect(dataPoint.value.buckets.boundaries).toEqual(
|
|
192
|
+
SEMCONV_HTTP_DURATION_BOUNDARIES,
|
|
193
|
+
);
|
|
194
|
+
expect(dataPoint.value.buckets.counts).toEqual([
|
|
195
|
+
1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0,
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("forwards description and unit options for counters and up-down counters", async () => {
|
|
200
|
+
const meter = makeMeter(
|
|
201
|
+
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
meter
|
|
205
|
+
.createCounter("requests.total", {
|
|
206
|
+
description: "Total requests",
|
|
207
|
+
unit: "1",
|
|
208
|
+
})
|
|
209
|
+
.add(1);
|
|
210
|
+
meter
|
|
211
|
+
.createUpDownCounter("users.active", {
|
|
212
|
+
description: "Active users",
|
|
213
|
+
unit: "{user}",
|
|
214
|
+
})
|
|
215
|
+
.add(1);
|
|
216
|
+
|
|
217
|
+
await meter.shutdown();
|
|
218
|
+
|
|
219
|
+
const exporter = lastExporter();
|
|
220
|
+
const counter = metricData(exporter, "metric-service.requests.total");
|
|
221
|
+
expect(counter?.descriptor.description).toBe("Total requests");
|
|
222
|
+
expect(counter?.descriptor.unit).toBe("1");
|
|
223
|
+
const upDownCounter = metricData(exporter, "metric-service.users.active");
|
|
224
|
+
expect(upDownCounter?.descriptor.description).toBe("Active users");
|
|
225
|
+
expect(upDownCounter?.descriptor.unit).toBe("{user}");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("accepts an options object for observable gauges and keeps them unprefixed", async () => {
|
|
229
|
+
const meter = makeMeter(
|
|
230
|
+
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
meter.createObservableGauge(
|
|
234
|
+
"cpu.usage",
|
|
235
|
+
(observableResult) => observableResult.observe(0.5),
|
|
236
|
+
{ description: "Current CPU usage", unit: "1" },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await meter.shutdown();
|
|
240
|
+
|
|
241
|
+
const exporter = lastExporter();
|
|
242
|
+
const names = metricNames(exporter);
|
|
243
|
+
expect(names).toContain("cpu.usage");
|
|
244
|
+
expect(names).not.toContain("metric-service.cpu.usage");
|
|
245
|
+
const gauge = metricData(exporter, "cpu.usage");
|
|
246
|
+
expect(gauge?.descriptor.description).toBe("Current CPU usage");
|
|
247
|
+
expect(gauge?.descriptor.unit).toBe("1");
|
|
135
248
|
});
|
|
136
249
|
|
|
137
250
|
it("creates a histogram without the service-name prefix when the unprefixed option is set", async () => {
|
package/src/StandardMeter.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Histogram,
|
|
4
4
|
Meter,
|
|
5
5
|
metrics,
|
|
6
|
+
type MetricOptions,
|
|
6
7
|
ObservableGauge,
|
|
7
8
|
UpDownCounter,
|
|
8
9
|
} from "@opentelemetry/api";
|
|
@@ -21,8 +22,11 @@ const ALREADY_REGISTERED_WARNING =
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Options accepted by {@link StandardMeter.createHistogram}.
|
|
25
|
+
* `description`, `unit`, `valueType` and `advice` are forwarded to the
|
|
26
|
+
* OpenTelemetry Meter; `advice.explicitBucketBoundaries` sets the histogram
|
|
27
|
+
* bucket boundaries.
|
|
24
28
|
*/
|
|
25
|
-
export interface StandardMeterCreateHistogramOptions {
|
|
29
|
+
export interface StandardMeterCreateHistogramOptions extends MetricOptions {
|
|
26
30
|
/**
|
|
27
31
|
* Create the histogram without the `${serviceName}.` prefix:
|
|
28
32
|
* exported exactly as `key`.
|
|
@@ -85,20 +89,25 @@ export class StandardMeter {
|
|
|
85
89
|
return this.shutdownPromise;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
public createCounter(key: string): Counter {
|
|
89
|
-
return this.meter.createCounter(`${this.serviceName}.${key}
|
|
92
|
+
public createCounter(key: string, options?: MetricOptions): Counter {
|
|
93
|
+
return this.meter.createCounter(`${this.serviceName}.${key}`, options);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
public createUpDownCounter(
|
|
93
|
-
|
|
96
|
+
public createUpDownCounter(
|
|
97
|
+
key: string,
|
|
98
|
+
options?: MetricOptions,
|
|
99
|
+
): UpDownCounter {
|
|
100
|
+
return this.meter.createUpDownCounter(`${this.serviceName}.${key}`, options);
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
public createHistogram(
|
|
97
104
|
key: string,
|
|
98
105
|
options?: StandardMeterCreateHistogramOptions,
|
|
99
106
|
): Histogram {
|
|
107
|
+
const { unprefixed, ...instrumentOptions } = options ?? {};
|
|
100
108
|
return this.meter.createHistogram(
|
|
101
|
-
|
|
109
|
+
unprefixed ? key : `${this.serviceName}.${key}`,
|
|
110
|
+
instrumentOptions,
|
|
102
111
|
);
|
|
103
112
|
}
|
|
104
113
|
|
|
@@ -106,11 +115,13 @@ export class StandardMeter {
|
|
|
106
115
|
key: string,
|
|
107
116
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
117
|
callback: (observableResult: any) => void,
|
|
109
|
-
|
|
118
|
+
descriptionOrOptions?: string | MetricOptions,
|
|
110
119
|
): ObservableGauge {
|
|
111
120
|
const observableGauge = this.meter.createObservableGauge(
|
|
112
121
|
key,
|
|
113
|
-
|
|
122
|
+
typeof descriptionOrOptions === "string"
|
|
123
|
+
? { description: descriptionOrOptions }
|
|
124
|
+
: descriptionOrOptions,
|
|
114
125
|
);
|
|
115
126
|
observableGauge.addCallback(callback);
|
|
116
127
|
return observableGauge;
|