@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 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` with name `${serviceName}.${key}`.
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, description?)` — Creates an `ObservableGauge` with a callback. Description is optional.
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, description?: string): ObservableGauge;
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
- return this.meter.createHistogram((options === null || options === void 0 ? void 0 : options.unprefixed) ? key : `${this.serviceName}.${key}`);
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, description) {
67
- const observableGauge = this.meter.createObservableGauge(key, description ? { description } : undefined);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@devopsplaybook.io/otel-utils",
3
- "version": "1.6.0",
3
+ "version": "1.7.0-beta.38.e32e141",
4
4
  "description": "Utility to simplify integration with Open Telemetry",
5
5
  "keywords": [
6
6
  "Open",
@@ -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
- expect(metricNames(lastExporter())).toEqual(
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 () => {
@@ -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(key: string): UpDownCounter {
93
- return this.meter.createUpDownCounter(`${this.serviceName}.${key}`);
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
- options?.unprefixed ? key : `${this.serviceName}.${key}`,
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
- description?: string,
118
+ descriptionOrOptions?: string | MetricOptions,
110
119
  ): ObservableGauge {
111
120
  const observableGauge = this.meter.createObservableGauge(
112
121
  key,
113
- description ? { description } : undefined,
122
+ typeof descriptionOrOptions === "string"
123
+ ? { description: descriptionOrOptions }
124
+ : descriptionOrOptions,
114
125
  );
115
126
  observableGauge.addCallback(callback);
116
127
  return observableGauge;