@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.
@@ -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 prom_client_1 = require("prom-client");
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
- * Get the service name from environment or use default
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
- return process.env[exports.ENV_KEY_SERVICE_NAME] || 'unknown_service';
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 must match [a-zA-Z_:][a-zA-Z0-9_:]*
94
+ * Prometheus/OTel metric names should match [a-zA-Z_:][a-zA-Z0-9_:]*
60
95
  * This function:
61
- * - Converts to lowercase (Prometheus convention)
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 label to existing labelNames if not already present
111
+ * Adds service_name attribute to provided attributes
84
112
  */
85
- function ensureServiceNameLabel(labelNames) {
86
- const labels = labelNames ? [...labelNames] : [];
87
- if (!labels.includes('service_name')) {
88
- labels.unshift('service_name');
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 labels;
126
+ return result;
91
127
  }
92
128
  /**
93
- * Wraps a Counter to automatically inject service_name label
129
+ * Wraps an OTel Counter to automatically inject service_name attribute
94
130
  */
95
131
  class ServiceAwareCounter {
96
- constructor(config, serviceName) {
97
- this.counter = new prom_client_1.Counter(config);
98
- this.serviceName = serviceName;
132
+ constructor(counter) {
133
+ this.counter = counter;
99
134
  }
100
- inc(labelsOrValue, value) {
135
+ inc(labelsOrValue, maybeValue) {
136
+ let value;
137
+ let labels;
101
138
  if (typeof labelsOrValue === 'number') {
102
- // inc(value) signature
103
- this.counter.inc({ service_name: this.serviceName }, labelsOrValue);
139
+ value = labelsOrValue;
140
+ labels = undefined;
104
141
  }
105
142
  else {
106
- // inc(labels, value) signature
107
- const labelsWithService = { ...labelsOrValue, service_name: this.serviceName };
108
- this.counter.inc(labelsWithService, value);
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
- this.counter.reset();
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 a Histogram to automatically inject service_name label
156
+ * Wraps an OTel Histogram to automatically inject service_name attribute
121
157
  */
122
158
  class ServiceAwareHistogram {
123
- constructor(config, serviceName) {
124
- this.histogram = new prom_client_1.Histogram(config);
125
- this.serviceName = serviceName;
159
+ constructor(histogram) {
160
+ this.histogram = histogram;
126
161
  }
127
- observe(labelsOrValue, value) {
162
+ observe(labelsOrValue, maybeValue) {
128
163
  if (typeof labelsOrValue === 'number') {
129
- // observe(value) signature
130
- this.histogram.observe({ service_name: this.serviceName }, labelsOrValue);
164
+ this.histogram.record(labelsOrValue, withServiceName());
131
165
  }
132
- else if (value !== undefined) {
133
- // observe(labels, value) signature
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
- // Invalid call: labels provided but value is missing
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
- this.histogram.reset();
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 labelsWithService = { ...labels, service_name: this.serviceName };
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 completionWithService = {
156
- ...completionLabels,
157
- service_name: this.serviceName,
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(labels) {
163
- const labelsWithService = { ...labels, service_name: this.serviceName };
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 a Gauge to automatically inject service_name label
189
+ * Wraps an OTel UpDownCounter to emulate gauge set/inc/dec with service_name.
169
190
  */
170
191
  class ServiceAwareGauge {
171
- constructor(config, serviceName) {
172
- this.gauge = new prom_client_1.Gauge(config);
173
- this.serviceName = serviceName;
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
- inc(labelsOrValue, value) {
179
- if (typeof labelsOrValue === 'number') {
180
- this.gauge.inc({ service_name: this.serviceName }, labelsOrValue);
181
- }
182
- else {
183
- this.gauge.inc(this.addServiceLabel(labelsOrValue), value);
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
- dec(labelsOrValue, value) {
204
+ parseArgs(labelsOrValue, maybeValue, defaultValue = 1) {
187
205
  if (typeof labelsOrValue === 'number') {
188
- this.gauge.dec({ service_name: this.serviceName }, labelsOrValue);
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
- set(labelsOrValue, value) {
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
- this.gauge.set({ service_name: this.serviceName }, labelsOrValue);
233
+ labels = {};
234
+ newValue = labelsOrValue;
197
235
  }
198
- else if (value !== undefined) {
199
- this.gauge.set(this.addServiceLabel(labelsOrValue), value);
236
+ else if (typeof maybeValue === 'number') {
237
+ labels = labelsOrValue;
238
+ newValue = maybeValue;
200
239
  }
201
240
  else {
202
- // Invalid call: labels provided but value is missing
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.gauge.setToCurrentTime(this.addServiceLabel(labels));
251
+ this.set(labels || {}, Date.now() / 1000);
209
252
  }
210
253
  startTimer(labels) {
211
- const endTimer = this.gauge.startTimer(this.addServiceLabel(labels));
212
- // Return wrapper to ensure service_name cannot be overridden on completion
254
+ const start = globalThis.performance.now();
213
255
  return (completionLabels) => {
214
- return endTimer(this.addServiceLabel(completionLabels));
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
- this.gauge.reset();
219
- }
220
- remove(...labelValues) {
221
- // Prepend service_name to match how set()/inc()/dec() add it
222
- this.gauge.remove(this.serviceName, ...labelValues);
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
- // Auto-suffix with _total if not already present (prevents duplication)
275
- const name = normalizedName.endsWith('_total')
307
+ const fullName = normalizedName.endsWith('_total')
276
308
  ? normalizedName
277
309
  : `${normalizedName}_total`;
278
- // Add service_name to labelNames
279
- const labelNames = ensureServiceNameLabel(config.labelNames);
280
- return new ServiceAwareCounter({
281
- ...config,
282
- name,
283
- labelNames,
284
- registers: [(0, registry_1.getRegistry)()],
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
- config.buckets.some((bucket) => typeof bucket !== 'number' || bucket < 0)) {
364
- throw new Error('All histogram buckets must be non-negative numbers');
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
- // Add service_name to labelNames
400
- const labelNames = ensureServiceNameLabel(config.labelNames);
401
- // Remove the custom 'unit' property before passing to Histogram constructor
402
- const { unit: _, ...histogramConfig } = config;
403
- return new ServiceAwareHistogram({
404
- ...histogramConfig,
405
- name,
406
- labelNames,
407
- registers: [(0, registry_1.getRegistry)()],
408
- }, getServiceName());
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
- // Add service_name to labelNames
454
- const labelNames = ensureServiceNameLabel(config.labelNames);
455
- return new ServiceAwareGauge({
456
- ...config,
457
- name,
458
- labelNames,
459
- registers: [(0, registry_1.getRegistry)()],
460
- }, getServiceName());
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