@manifest-cyber/observability-ts 0.2.11 → 0.2.13
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 +67 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +39 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +70 -1
- package/dist/init.js.map +1 -1
- package/dist/metrics/builders.d.ts +46 -155
- package/dist/metrics/builders.d.ts.map +1 -1
- package/dist/metrics/builders.js +207 -260
- package/dist/metrics/builders.js.map +1 -1
- package/dist/metrics/index.d.ts +5 -5
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +9 -7
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instrumentation.d.ts +9 -9
- package/dist/metrics/instrumentation.d.ts.map +1 -1
- package/dist/metrics/instrumentation.js.map +1 -1
- package/dist/metrics/provider.d.ts +98 -0
- package/dist/metrics/provider.d.ts.map +1 -0
- package/dist/metrics/provider.js +462 -0
- package/dist/metrics/provider.js.map +1 -0
- package/dist/metrics/server.d.ts +1 -1
- package/dist/metrics/server.d.ts.map +1 -1
- package/dist/metrics/server.js +52 -15
- package/dist/metrics/server.js.map +1 -1
- package/dist/tracing/setup.d.ts.map +1 -1
- package/dist/tracing/setup.js +77 -29
- package/dist/tracing/setup.js.map +1 -1
- package/package.json +34 -26
- package/dist/metrics/registry.d.ts +0 -36
- package/dist/metrics/registry.d.ts.map +0 -1
- package/dist/metrics/registry.js +0 -48
- package/dist/metrics/registry.js.map +0 -1
package/dist/metrics/builders.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BUCKETS = exports.ENV_KEY_SERVICE_NAME = void 0;
|
|
4
|
+
exports.resetCachedServiceName = resetCachedServiceName;
|
|
5
|
+
exports.setServiceName = setServiceName;
|
|
4
6
|
exports.createCounter = createCounter;
|
|
5
7
|
exports.createHistogram = createHistogram;
|
|
6
8
|
exports.createGauge = createGauge;
|
|
7
|
-
const
|
|
8
|
-
const registry_1 = require("./registry");
|
|
9
|
+
const api_1 = require("@opentelemetry/api");
|
|
9
10
|
/**
|
|
10
11
|
* Environment variable key for service name
|
|
11
12
|
* Should match the key used in @manifest-cyber/logger-ts
|
|
@@ -48,217 +49,250 @@ exports.BUCKETS = {
|
|
|
48
49
|
},
|
|
49
50
|
};
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
+
* Cached service name to avoid repeated process.env lookups
|
|
53
|
+
*/
|
|
54
|
+
let cachedServiceName;
|
|
55
|
+
/**
|
|
56
|
+
* Get the service name from environment or use default.
|
|
57
|
+
* Reads from process.env on each call to pick up runtime changes,
|
|
58
|
+
* falling back to explicitly set cachedServiceName or 'unknown_service'.
|
|
52
59
|
*/
|
|
53
60
|
function getServiceName() {
|
|
54
|
-
|
|
61
|
+
// Check environment first to allow runtime changes
|
|
62
|
+
const envServiceName = process.env[exports.ENV_KEY_SERVICE_NAME];
|
|
63
|
+
if (envServiceName) {
|
|
64
|
+
return envServiceName;
|
|
65
|
+
}
|
|
66
|
+
// Fall back to explicitly set service name
|
|
67
|
+
if (cachedServiceName !== undefined) {
|
|
68
|
+
return cachedServiceName;
|
|
69
|
+
}
|
|
70
|
+
// Final fallback
|
|
71
|
+
return 'unknown_service';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reset cached service name (for testing)
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
function resetCachedServiceName() {
|
|
78
|
+
cachedServiceName = undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Set service name explicitly (used as fallback when environment variable is not set)
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
function setServiceName(name) {
|
|
85
|
+
const cleaned = name?.trim();
|
|
86
|
+
if (!cleaned) {
|
|
87
|
+
throw new Error('Service name cannot be empty or whitespace-only');
|
|
88
|
+
}
|
|
89
|
+
cachedServiceName = cleaned;
|
|
55
90
|
}
|
|
56
91
|
/**
|
|
57
92
|
* Normalize a metric name component to be Prometheus-compatible
|
|
58
93
|
*
|
|
59
|
-
* Prometheus metric names
|
|
94
|
+
* Prometheus/OTel metric names should match [a-zA-Z_:][a-zA-Z0-9_:]*
|
|
60
95
|
* This function:
|
|
61
|
-
* - Converts to lowercase
|
|
96
|
+
* - Converts to lowercase
|
|
62
97
|
* - Replaces hyphens and spaces with underscores
|
|
63
98
|
* - Removes invalid characters
|
|
64
99
|
* - Ensures the name starts with a letter or underscore
|
|
65
|
-
*
|
|
66
|
-
* @param name Name component to normalize
|
|
67
|
-
* @returns Normalized name safe for Prometheus
|
|
68
100
|
*/
|
|
69
101
|
function normalizeMetricName(name) {
|
|
70
|
-
// Convert to lowercase (Prometheus convention)
|
|
71
102
|
let normalized = name.toLowerCase();
|
|
72
|
-
// Replace hyphens and spaces with underscores
|
|
73
103
|
normalized = normalized.replace(/[-\s]/g, '_');
|
|
74
|
-
// Remove any characters that aren't alphanumeric, underscore, or colon
|
|
75
104
|
normalized = normalized.replace(/[^a-z0-9_:]/g, '_');
|
|
76
|
-
// Ensure it starts with a letter or underscore
|
|
77
105
|
if (!/^[a-z_]/.test(normalized)) {
|
|
78
106
|
normalized = `_${normalized}`;
|
|
79
107
|
}
|
|
80
108
|
return normalized;
|
|
81
109
|
}
|
|
82
110
|
/**
|
|
83
|
-
* Adds service_name
|
|
111
|
+
* Adds service_name attribute to provided attributes
|
|
84
112
|
*/
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
|
|
113
|
+
function withServiceName(attrs) {
|
|
114
|
+
const result = { service_name: getServiceName() };
|
|
115
|
+
if (attrs) {
|
|
116
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
117
|
+
// Skip service_name to prevent overriding the enforced value
|
|
118
|
+
if (key === 'service_name') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
result[key] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
89
125
|
}
|
|
90
|
-
return
|
|
126
|
+
return result;
|
|
91
127
|
}
|
|
92
128
|
/**
|
|
93
|
-
* Wraps
|
|
129
|
+
* Wraps an OTel Counter to automatically inject service_name attribute
|
|
94
130
|
*/
|
|
95
131
|
class ServiceAwareCounter {
|
|
96
|
-
constructor(
|
|
97
|
-
this.counter =
|
|
98
|
-
this.serviceName = serviceName;
|
|
132
|
+
constructor(counter) {
|
|
133
|
+
this.counter = counter;
|
|
99
134
|
}
|
|
100
|
-
inc(labelsOrValue,
|
|
135
|
+
inc(labelsOrValue, maybeValue) {
|
|
136
|
+
let value;
|
|
137
|
+
let labels;
|
|
101
138
|
if (typeof labelsOrValue === 'number') {
|
|
102
|
-
|
|
103
|
-
|
|
139
|
+
value = labelsOrValue;
|
|
140
|
+
labels = undefined;
|
|
104
141
|
}
|
|
105
142
|
else {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
value = typeof maybeValue === 'number' ? maybeValue : 1;
|
|
144
|
+
labels = labelsOrValue;
|
|
145
|
+
}
|
|
146
|
+
if (value < 0) {
|
|
147
|
+
throw new RangeError(`Counter increment must be non-negative, got ${value}. Counters can only increase.`);
|
|
109
148
|
}
|
|
149
|
+
this.counter.add(value, withServiceName(labels));
|
|
110
150
|
}
|
|
111
151
|
reset() {
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
remove(...labelValues) {
|
|
115
|
-
// Prepend service_name to match how inc() adds it
|
|
116
|
-
this.counter.remove(this.serviceName, ...labelValues);
|
|
152
|
+
// No-op: OTel counters cannot be reset; method kept for API compatibility
|
|
117
153
|
}
|
|
118
154
|
}
|
|
119
155
|
/**
|
|
120
|
-
* Wraps
|
|
156
|
+
* Wraps an OTel Histogram to automatically inject service_name attribute
|
|
121
157
|
*/
|
|
122
158
|
class ServiceAwareHistogram {
|
|
123
|
-
constructor(
|
|
124
|
-
this.histogram =
|
|
125
|
-
this.serviceName = serviceName;
|
|
159
|
+
constructor(histogram) {
|
|
160
|
+
this.histogram = histogram;
|
|
126
161
|
}
|
|
127
|
-
observe(labelsOrValue,
|
|
162
|
+
observe(labelsOrValue, maybeValue) {
|
|
128
163
|
if (typeof labelsOrValue === 'number') {
|
|
129
|
-
|
|
130
|
-
this.histogram.observe({ service_name: this.serviceName }, labelsOrValue);
|
|
164
|
+
this.histogram.record(labelsOrValue, withServiceName());
|
|
131
165
|
}
|
|
132
|
-
else if (
|
|
133
|
-
|
|
134
|
-
const labelsWithService = { ...labelsOrValue, service_name: this.serviceName };
|
|
135
|
-
this.histogram.observe(labelsWithService, value);
|
|
166
|
+
else if (typeof maybeValue === 'number') {
|
|
167
|
+
this.histogram.record(maybeValue, withServiceName(labelsOrValue));
|
|
136
168
|
}
|
|
137
169
|
else {
|
|
138
|
-
|
|
139
|
-
throw new Error('ServiceAwareHistogram.observe() called with labels but missing value. ' +
|
|
140
|
-
'Usage: observe(value) or observe(labels, value)');
|
|
170
|
+
throw new Error('ServiceAwareHistogram.observe() called with labels but missing value. Usage: observe(value) or observe(labels, value)');
|
|
141
171
|
}
|
|
142
172
|
}
|
|
143
173
|
reset() {
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
remove(...labelValues) {
|
|
147
|
-
// Prepend service_name to match how observe() adds it
|
|
148
|
-
this.histogram.remove(this.serviceName, ...labelValues);
|
|
174
|
+
// No-op: OTel histograms cannot be reset; method kept for API compatibility
|
|
149
175
|
}
|
|
150
176
|
startTimer(labels) {
|
|
151
|
-
const
|
|
152
|
-
const endTimer = this.histogram.startTimer(labelsWithService);
|
|
153
|
-
// Return wrapper to ensure service_name cannot be overridden on completion
|
|
177
|
+
const start = globalThis.performance.now();
|
|
154
178
|
return (completionLabels) => {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
return endTimer(completionWithService);
|
|
179
|
+
const duration = (globalThis.performance.now() - start) / 1000;
|
|
180
|
+
this.observe({ ...(labels || {}), ...(completionLabels || {}) }, duration);
|
|
181
|
+
return duration;
|
|
160
182
|
};
|
|
161
183
|
}
|
|
162
|
-
zero(
|
|
163
|
-
|
|
164
|
-
this.histogram.zero(labelsWithService);
|
|
184
|
+
zero(_labels) {
|
|
185
|
+
// No-op: Histograms do not support explicit zeroing in OTel
|
|
165
186
|
}
|
|
166
187
|
}
|
|
167
188
|
/**
|
|
168
|
-
* Wraps
|
|
189
|
+
* Wraps an OTel UpDownCounter to emulate gauge set/inc/dec with service_name.
|
|
169
190
|
*/
|
|
170
191
|
class ServiceAwareGauge {
|
|
171
|
-
constructor(
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
174
|
-
}
|
|
175
|
-
addServiceLabel(labels) {
|
|
176
|
-
return { ...labels, service_name: this.serviceName };
|
|
192
|
+
constructor(upDownCounter) {
|
|
193
|
+
this.upDownCounter = upDownCounter;
|
|
194
|
+
this.values = new Map();
|
|
177
195
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Generate a stable key for label sets by sorting entries and serializing to JSON.
|
|
198
|
+
* Note: numeric and string label values are treated as distinct (e.g., 1 vs "1").
|
|
199
|
+
*/
|
|
200
|
+
keyFromLabels(labels) {
|
|
201
|
+
const sortedEntries = Object.entries(labels).sort(([a], [b]) => a.localeCompare(b));
|
|
202
|
+
return JSON.stringify(Object.fromEntries(sortedEntries));
|
|
185
203
|
}
|
|
186
|
-
|
|
204
|
+
parseArgs(labelsOrValue, maybeValue, defaultValue = 1) {
|
|
187
205
|
if (typeof labelsOrValue === 'number') {
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
this.gauge.dec(this.addServiceLabel(labelsOrValue), value);
|
|
206
|
+
return { labels: {}, value: labelsOrValue };
|
|
192
207
|
}
|
|
208
|
+
return {
|
|
209
|
+
labels: labelsOrValue || {},
|
|
210
|
+
value: typeof maybeValue === 'number' ? maybeValue : defaultValue,
|
|
211
|
+
};
|
|
193
212
|
}
|
|
194
|
-
|
|
213
|
+
inc(labelsOrValue, maybeValue) {
|
|
214
|
+
const { labels, value } = this.parseArgs(labelsOrValue, maybeValue);
|
|
215
|
+
const attrs = withServiceName(labels);
|
|
216
|
+
const key = this.keyFromLabels(attrs);
|
|
217
|
+
const previousValue = this.values.get(key) ?? 0;
|
|
218
|
+
this.values.set(key, previousValue + value);
|
|
219
|
+
this.upDownCounter.add(value, attrs);
|
|
220
|
+
}
|
|
221
|
+
dec(labelsOrValue, maybeValue) {
|
|
222
|
+
const { labels, value } = this.parseArgs(labelsOrValue, maybeValue);
|
|
223
|
+
const attrs = withServiceName(labels);
|
|
224
|
+
const key = this.keyFromLabels(attrs);
|
|
225
|
+
const previousValue = this.values.get(key) ?? 0;
|
|
226
|
+
this.values.set(key, previousValue - value);
|
|
227
|
+
this.upDownCounter.add(-value, attrs);
|
|
228
|
+
}
|
|
229
|
+
set(labelsOrValue, maybeValue) {
|
|
230
|
+
let labels;
|
|
231
|
+
let newValue;
|
|
195
232
|
if (typeof labelsOrValue === 'number') {
|
|
196
|
-
|
|
233
|
+
labels = {};
|
|
234
|
+
newValue = labelsOrValue;
|
|
197
235
|
}
|
|
198
|
-
else if (
|
|
199
|
-
|
|
236
|
+
else if (typeof maybeValue === 'number') {
|
|
237
|
+
labels = labelsOrValue;
|
|
238
|
+
newValue = maybeValue;
|
|
200
239
|
}
|
|
201
240
|
else {
|
|
202
|
-
|
|
203
|
-
throw new Error('ServiceAwareGauge.set() called with labels but missing value. ' +
|
|
204
|
-
'Usage: set(value) or set(labels, value)');
|
|
241
|
+
throw new Error('ServiceAwareGauge.set() called with labels but missing value. Usage: set(value) or set(labels, value)');
|
|
205
242
|
}
|
|
243
|
+
const attrs = withServiceName(labels);
|
|
244
|
+
const key = this.keyFromLabels(attrs);
|
|
245
|
+
const previousValue = this.values.get(key) ?? 0;
|
|
246
|
+
const delta = newValue - previousValue;
|
|
247
|
+
this.values.set(key, newValue);
|
|
248
|
+
this.upDownCounter.add(delta, attrs);
|
|
206
249
|
}
|
|
207
250
|
setToCurrentTime(labels) {
|
|
208
|
-
this.
|
|
251
|
+
this.set(labels || {}, Date.now() / 1000);
|
|
209
252
|
}
|
|
210
253
|
startTimer(labels) {
|
|
211
|
-
const
|
|
212
|
-
// Return wrapper to ensure service_name cannot be overridden on completion
|
|
254
|
+
const start = globalThis.performance.now();
|
|
213
255
|
return (completionLabels) => {
|
|
214
|
-
|
|
256
|
+
const duration = (globalThis.performance.now() - start) / 1000;
|
|
257
|
+
this.set({ ...(labels || {}), ...(completionLabels || {}) }, duration);
|
|
258
|
+
return duration;
|
|
215
259
|
};
|
|
216
260
|
}
|
|
217
261
|
reset() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
262
|
+
// Emit compensating deltas to zero out the underlying UpDownCounter
|
|
263
|
+
for (const [key, previousValue] of this.values.entries()) {
|
|
264
|
+
const attrs = JSON.parse(key);
|
|
265
|
+
this.upDownCounter.add(-previousValue, attrs);
|
|
266
|
+
}
|
|
267
|
+
this.values.clear();
|
|
268
|
+
}
|
|
269
|
+
remove(name) {
|
|
270
|
+
// Build labels object from name or use it directly
|
|
271
|
+
let labels;
|
|
272
|
+
if (typeof name === 'string') {
|
|
273
|
+
// If a single name is provided, treat it as the first label value
|
|
274
|
+
// This is a simplified implementation - full prom-client compatibility would need label ordering
|
|
275
|
+
labels = {};
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
labels = name || {};
|
|
279
|
+
}
|
|
280
|
+
const attrs = withServiceName(labels);
|
|
281
|
+
const key = this.keyFromLabels(attrs);
|
|
282
|
+
const storedValue = this.values.get(key);
|
|
283
|
+
if (storedValue !== undefined) {
|
|
284
|
+
// Undo the metric contribution by subtracting the stored value
|
|
285
|
+
this.upDownCounter.add(-storedValue, attrs);
|
|
286
|
+
// Remove from the map
|
|
287
|
+
this.values.delete(key);
|
|
288
|
+
}
|
|
289
|
+
// If not present, no-op (guard against double-removal)
|
|
223
290
|
}
|
|
224
291
|
}
|
|
225
292
|
/**
|
|
226
293
|
* Creates a counter metric
|
|
227
|
-
*
|
|
228
|
-
* Counters are cumulative metrics that only increase.
|
|
229
|
-
* Use for counting events like requests, errors, items processed, etc.
|
|
230
|
-
*
|
|
231
|
-
* The metric name will be automatically suffixed with '_total' if not already present.
|
|
232
|
-
* A service_name label is automatically added to all observations.
|
|
233
|
-
*
|
|
234
|
-
* @param config Counter configuration (name, help, labelNames)
|
|
235
|
-
* @returns A new Counter instance registered with the custom registry
|
|
236
|
-
*
|
|
237
|
-
* @example
|
|
238
|
-
* ```typescript
|
|
239
|
-
* import { createCounter } from '@manifest-cyber/observability-ts';
|
|
240
|
-
*
|
|
241
|
-
* // Simple counter - automatically suffixed with _total
|
|
242
|
-
* export const requestsTotal = createCounter({
|
|
243
|
-
* name: 'http_requests',
|
|
244
|
-
* help: 'Total number of HTTP requests',
|
|
245
|
-
* labelNames: ['method', 'status'],
|
|
246
|
-
* });
|
|
247
|
-
*
|
|
248
|
-
* // Or provide the full name (won't duplicate suffix)
|
|
249
|
-
* export const requestsTotal = createCounter({
|
|
250
|
-
* name: 'http_requests_total',
|
|
251
|
-
* help: 'Total number of HTTP requests',
|
|
252
|
-
* labelNames: ['method', 'status'],
|
|
253
|
-
* });
|
|
254
|
-
*
|
|
255
|
-
* // Later in code (service_name is automatically added):
|
|
256
|
-
* requestsTotal.inc({ method: 'GET', status: '200' });
|
|
257
|
-
* // Results in: http_requests_total{service_name="my-service",method="GET",status="200"}
|
|
258
|
-
* ```
|
|
259
294
|
*/
|
|
260
295
|
function createCounter(config) {
|
|
261
|
-
// Validate configuration
|
|
262
296
|
if (!config.name || config.name.trim().length === 0) {
|
|
263
297
|
throw new Error('Counter name is required and cannot be empty');
|
|
264
298
|
}
|
|
@@ -269,83 +303,22 @@ function createCounter(config) {
|
|
|
269
303
|
config.labelNames.some((label) => !label || label.trim().length === 0)) {
|
|
270
304
|
throw new Error('All label names must be non-empty strings');
|
|
271
305
|
}
|
|
272
|
-
// Normalize the metric name first
|
|
273
306
|
const normalizedName = normalizeMetricName(config.name);
|
|
274
|
-
|
|
275
|
-
const name = normalizedName.endsWith('_total')
|
|
307
|
+
const fullName = normalizedName.endsWith('_total')
|
|
276
308
|
? normalizedName
|
|
277
309
|
: `${normalizedName}_total`;
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}, getServiceName());
|
|
310
|
+
// Use the global metrics API which returns a no-op meter if not initialized.
|
|
311
|
+
// This allows builders to work without explicit initMetricsProvider() call.
|
|
312
|
+
const meter = api_1.metrics.getMeter(getServiceName());
|
|
313
|
+
const counter = meter.createCounter(fullName, {
|
|
314
|
+
description: config.help,
|
|
315
|
+
});
|
|
316
|
+
return new ServiceAwareCounter(counter);
|
|
286
317
|
}
|
|
287
318
|
/**
|
|
288
319
|
* Creates a histogram metric
|
|
289
|
-
*
|
|
290
|
-
* Histograms track distributions of values, commonly used for durations.
|
|
291
|
-
* They automatically track count, sum, and buckets.
|
|
292
|
-
*
|
|
293
|
-
* The metric name will be automatically suffixed with a unit (default: '_seconds') if not already present.
|
|
294
|
-
* A service_name label is automatically added to all observations.
|
|
295
|
-
*
|
|
296
|
-
* **Unit detection and auto-suffixing:**
|
|
297
|
-
* - If name already has a unit suffix (e.g., '_seconds', '_bytes'), it's used as-is
|
|
298
|
-
* - If name lacks a unit suffix, the `unit` parameter is appended (default: 'seconds')
|
|
299
|
-
* - If name has a unit AND `unit` parameter differs, a warning is logged and name's unit wins
|
|
300
|
-
*
|
|
301
|
-
* @param config Histogram configuration (name, help, labelNames, buckets, unit)
|
|
302
|
-
* @returns A new Histogram instance registered with the custom registry
|
|
303
|
-
*
|
|
304
|
-
* @example
|
|
305
|
-
* ```typescript
|
|
306
|
-
* import { createHistogram, BUCKETS } from '@manifest-cyber/observability-ts';
|
|
307
|
-
*
|
|
308
|
-
* // Duration metric - automatically suffixed with _seconds (default)
|
|
309
|
-
* export const requestDuration = createHistogram({
|
|
310
|
-
* name: 'http_request_duration',
|
|
311
|
-
* help: 'HTTP request duration in seconds',
|
|
312
|
-
* labelNames: ['method', 'route'],
|
|
313
|
-
* buckets: BUCKETS.DURATION.HTTP,
|
|
314
|
-
* });
|
|
315
|
-
* // Result: http_request_duration_seconds
|
|
316
|
-
*
|
|
317
|
-
* // File size metric with custom unit
|
|
318
|
-
* export const fileSize = createHistogram({
|
|
319
|
-
* name: 'file_size',
|
|
320
|
-
* help: 'File size in bytes',
|
|
321
|
-
* unit: 'bytes',
|
|
322
|
-
* buckets: BUCKETS.SIZE.MEDIUM,
|
|
323
|
-
* });
|
|
324
|
-
* // Result: file_size_bytes
|
|
325
|
-
*
|
|
326
|
-
* // Provide full name (won't duplicate suffix)
|
|
327
|
-
* export const fileSize2 = createHistogram({
|
|
328
|
-
* name: 'file_size_bytes',
|
|
329
|
-
* help: 'File size in bytes',
|
|
330
|
-
* buckets: BUCKETS.SIZE.MEDIUM,
|
|
331
|
-
* });
|
|
332
|
-
* // Result: file_size_bytes
|
|
333
|
-
*
|
|
334
|
-
* // Conflict scenario (logs warning, uses name's unit)
|
|
335
|
-
* export const duration = createHistogram({
|
|
336
|
-
* name: 'processing_time_milliseconds',
|
|
337
|
-
* unit: 'seconds', // Warning: conflicts with _milliseconds in name
|
|
338
|
-
* buckets: BUCKETS.DURATION.FAST,
|
|
339
|
-
* });
|
|
340
|
-
* // Result: processing_time_milliseconds (name wins, warning logged)
|
|
341
|
-
*
|
|
342
|
-
* // Later in code (service_name is automatically added):
|
|
343
|
-
* const duration = (Date.now() - start) / 1000;
|
|
344
|
-
* requestDuration.observe({ method: 'GET', route: '/api/users' }, duration);
|
|
345
|
-
* ```
|
|
346
320
|
*/
|
|
347
321
|
function createHistogram(config) {
|
|
348
|
-
// Validate configuration
|
|
349
322
|
if (!config.name || config.name.trim().length === 0) {
|
|
350
323
|
throw new Error('Histogram name is required and cannot be empty');
|
|
351
324
|
}
|
|
@@ -357,15 +330,21 @@ function createHistogram(config) {
|
|
|
357
330
|
throw new Error('All label names must be non-empty strings');
|
|
358
331
|
}
|
|
359
332
|
if (config.buckets && (!Array.isArray(config.buckets) || config.buckets.length === 0)) {
|
|
360
|
-
throw new Error('Histogram buckets must be a non-empty array');
|
|
361
|
-
}
|
|
362
|
-
if (config.buckets
|
|
363
|
-
|
|
364
|
-
|
|
333
|
+
throw new Error('Histogram buckets must be a non-empty array of finite, strictly increasing numbers');
|
|
334
|
+
}
|
|
335
|
+
if (config.buckets) {
|
|
336
|
+
// Validate all buckets are finite, non-negative numbers in strictly increasing order
|
|
337
|
+
for (let i = 0; i < config.buckets.length; i++) {
|
|
338
|
+
const bucket = config.buckets[i];
|
|
339
|
+
if (typeof bucket !== 'number' || !Number.isFinite(bucket) || bucket < 0) {
|
|
340
|
+
throw new Error('All histogram buckets must be finite, non-negative numbers in strictly increasing order');
|
|
341
|
+
}
|
|
342
|
+
if (i > 0 && bucket <= config.buckets[i - 1]) {
|
|
343
|
+
throw new Error('All histogram buckets must be finite, non-negative numbers in strictly increasing order');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
365
346
|
}
|
|
366
|
-
// Normalize the metric name first to handle hyphens, spaces, etc.
|
|
367
347
|
const normalizedBase = normalizeMetricName(config.name);
|
|
368
|
-
// Check if normalized name already has a unit suffix
|
|
369
348
|
const commonUnits = [
|
|
370
349
|
'seconds',
|
|
371
350
|
'milliseconds',
|
|
@@ -379,7 +358,6 @@ function createHistogram(config) {
|
|
|
379
358
|
'ratio',
|
|
380
359
|
'count',
|
|
381
360
|
];
|
|
382
|
-
// Detect if normalized name already has a unit suffix and extract it
|
|
383
361
|
let detectedUnit;
|
|
384
362
|
for (const unit of commonUnits) {
|
|
385
363
|
if (normalizedBase.endsWith(`_${unit}`)) {
|
|
@@ -387,57 +365,27 @@ function createHistogram(config) {
|
|
|
387
365
|
break;
|
|
388
366
|
}
|
|
389
367
|
}
|
|
390
|
-
// Warn if there's a conflict between detected unit and provided unit parameter
|
|
391
368
|
if (detectedUnit && config.unit && config.unit !== detectedUnit) {
|
|
392
|
-
console.warn(`Warning: Histogram name '${config.name}' already contains unit suffix '_${detectedUnit}',
|
|
393
|
-
`but unit parameter is set to '${config.unit}'. Using unit from name: '${detectedUnit}'.`);
|
|
369
|
+
console.warn(`Warning: Histogram name '${config.name}' already contains unit suffix '_${detectedUnit}', but unit parameter is set to '${config.unit}'. Using unit from name: '${detectedUnit}'.`);
|
|
394
370
|
}
|
|
395
|
-
// Determine final unit and name with suffix
|
|
396
|
-
// Priority: 1) Unit in name (if present), 2) unit parameter, 3) default 'seconds'
|
|
397
371
|
const finalUnit = detectedUnit || config.unit || 'seconds';
|
|
398
372
|
const name = detectedUnit ? normalizedBase : `${normalizedBase}_${finalUnit}`;
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
373
|
+
// Use the global metrics API which returns a no-op meter if not initialized.
|
|
374
|
+
// This allows builders to work without explicit initMetricsProvider() call.
|
|
375
|
+
const meter = api_1.metrics.getMeter(getServiceName());
|
|
376
|
+
const histogram = meter.createHistogram(name, {
|
|
377
|
+
description: config.help,
|
|
378
|
+
unit: finalUnit,
|
|
379
|
+
advice: {
|
|
380
|
+
explicitBucketBoundaries: config.buckets,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return new ServiceAwareHistogram(histogram);
|
|
409
384
|
}
|
|
410
385
|
/**
|
|
411
386
|
* Creates a gauge metric
|
|
412
|
-
*
|
|
413
|
-
* Gauges represent a value that can go up or down.
|
|
414
|
-
* Use for measuring current state like memory usage, active connections, queue depth, etc.
|
|
415
|
-
*
|
|
416
|
-
* The metric name should follow Prometheus naming conventions.
|
|
417
|
-
* A service_name label is automatically added to all observations.
|
|
418
|
-
*
|
|
419
|
-
* @param config Gauge configuration (name, help, labelNames)
|
|
420
|
-
* @returns A new Gauge instance registered with the custom registry
|
|
421
|
-
*
|
|
422
|
-
* @example
|
|
423
|
-
* ```typescript
|
|
424
|
-
* import { createGauge } from '@manifest-cyber/observability-ts';
|
|
425
|
-
*
|
|
426
|
-
* // Simple gauge metric
|
|
427
|
-
* export const activeConnections = createGauge({
|
|
428
|
-
* name: 'active_connections',
|
|
429
|
-
* help: 'Number of active connections',
|
|
430
|
-
* labelNames: ['type'],
|
|
431
|
-
* });
|
|
432
|
-
*
|
|
433
|
-
* // Later in code (service_name is automatically added):
|
|
434
|
-
* activeConnections.inc({ type: 'websocket' }); // increment
|
|
435
|
-
* activeConnections.dec({ type: 'websocket' }); // decrement
|
|
436
|
-
* activeConnections.set({ type: 'http' }, 42); // set to specific value
|
|
437
|
-
* ```
|
|
438
387
|
*/
|
|
439
388
|
function createGauge(config) {
|
|
440
|
-
// Validate configuration
|
|
441
389
|
if (!config.name || config.name.trim().length === 0) {
|
|
442
390
|
throw new Error('Gauge name is required and cannot be empty');
|
|
443
391
|
}
|
|
@@ -448,15 +396,14 @@ function createGauge(config) {
|
|
|
448
396
|
config.labelNames.some((label) => !label || label.trim().length === 0)) {
|
|
449
397
|
throw new Error('All label names must be non-empty strings');
|
|
450
398
|
}
|
|
451
|
-
// Normalize the metric name
|
|
452
399
|
const name = normalizeMetricName(config.name);
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
400
|
+
// Use the global metrics API which returns a no-op meter if not initialized.
|
|
401
|
+
// This allows builders to work without explicit initMetricsProvider() call.
|
|
402
|
+
const meter = api_1.metrics.getMeter(getServiceName());
|
|
403
|
+
// Use UpDownCounter to emulate a gauge (supports inc/dec/set operations)
|
|
404
|
+
const upDownCounter = meter.createUpDownCounter(name, {
|
|
405
|
+
description: config.help,
|
|
406
|
+
});
|
|
407
|
+
return new ServiceAwareGauge(upDownCounter);
|
|
461
408
|
}
|
|
462
409
|
//# sourceMappingURL=builders.js.map
|