@aleph-ai/tinyaleph 1.4.1 → 1.4.2
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/package.json +6 -1
- package/telemetry/metrics.js +708 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-ai/tinyaleph",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Prime-resonant semantic computing framework - hypercomplex algebra, oscillator dynamics, and entropy-minimizing reasoning",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"./engine": {
|
|
26
26
|
"require": "./engine/index.js",
|
|
27
27
|
"import": "./engine/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./telemetry": {
|
|
30
|
+
"require": "./telemetry/metrics.js",
|
|
31
|
+
"import": "./telemetry/metrics.js"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"scripts": {
|
|
@@ -84,6 +88,7 @@
|
|
|
84
88
|
"physics/",
|
|
85
89
|
"backends/",
|
|
86
90
|
"engine/",
|
|
91
|
+
"telemetry/",
|
|
87
92
|
"types/",
|
|
88
93
|
"docs/",
|
|
89
94
|
"LICENSE",
|
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Metrics for TinyAleph
|
|
3
|
+
*
|
|
4
|
+
* Provides Prometheus/OpenTelemetry-compatible metric types:
|
|
5
|
+
* - Counter: Monotonically increasing value
|
|
6
|
+
* - Gauge: Value that can go up and down
|
|
7
|
+
* - Histogram: Distribution of values in buckets
|
|
8
|
+
* - Summary: Quantile distribution
|
|
9
|
+
*
|
|
10
|
+
* Browser-compatible: No Node.js dependencies (HTTP server removed).
|
|
11
|
+
* Server endpoints should be implemented in the application layer.
|
|
12
|
+
* Extracted from apps/sentient/lib/telemetry.js for library reuse.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { SimpleEventEmitter } = require('../core/errors');
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// METRIC TYPES
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const MetricType = {
|
|
22
|
+
COUNTER: 'counter',
|
|
23
|
+
GAUGE: 'gauge',
|
|
24
|
+
HISTOGRAM: 'histogram',
|
|
25
|
+
SUMMARY: 'summary'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// BASE METRIC CLASS
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base metric class
|
|
34
|
+
*/
|
|
35
|
+
class Metric {
|
|
36
|
+
constructor(name, options = {}) {
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.help = options.help || '';
|
|
39
|
+
this.labels = options.labels || [];
|
|
40
|
+
this.type = MetricType.COUNTER;
|
|
41
|
+
this.values = new Map(); // labelKey -> value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get label key for a set of labels
|
|
46
|
+
* @param {Object} labels - Label values
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
getLabelKey(labels = {}) {
|
|
50
|
+
if (this.labels.length === 0) return '';
|
|
51
|
+
return this.labels.map(l => labels[l] || '').join(',');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format labels for Prometheus exposition
|
|
56
|
+
* @param {Object} labels - Label values
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
formatLabels(labels = {}) {
|
|
60
|
+
if (this.labels.length === 0) return '';
|
|
61
|
+
const pairs = this.labels
|
|
62
|
+
.filter(l => labels[l] !== undefined)
|
|
63
|
+
.map(l => `${l}="${labels[l]}"`);
|
|
64
|
+
return pairs.length > 0 ? `{${pairs.join(',')}}` : '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Export to Prometheus format
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
toPrometheus() {
|
|
72
|
+
throw new Error('toPrometheus must be implemented');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Export to OpenTelemetry format
|
|
77
|
+
* @returns {Object}
|
|
78
|
+
*/
|
|
79
|
+
toOTLP() {
|
|
80
|
+
throw new Error('toOTLP must be implemented');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reset metric
|
|
85
|
+
*/
|
|
86
|
+
reset() {
|
|
87
|
+
this.values.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// COUNTER
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Counter metric - monotonically increasing value
|
|
97
|
+
*/
|
|
98
|
+
class Counter extends Metric {
|
|
99
|
+
constructor(name, options = {}) {
|
|
100
|
+
super(name, options);
|
|
101
|
+
this.type = MetricType.COUNTER;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Increment the counter
|
|
106
|
+
* @param {number} value - Amount to increment (default 1)
|
|
107
|
+
* @param {Object} labels - Label values
|
|
108
|
+
*/
|
|
109
|
+
inc(value = 1, labels = {}) {
|
|
110
|
+
if (value < 0) {
|
|
111
|
+
throw new Error('Counter can only be incremented');
|
|
112
|
+
}
|
|
113
|
+
const key = this.getLabelKey(labels);
|
|
114
|
+
const current = this.values.get(key) || { value: 0, labels };
|
|
115
|
+
current.value += value;
|
|
116
|
+
this.values.set(key, current);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get current value
|
|
121
|
+
* @param {Object} labels - Label values
|
|
122
|
+
* @returns {number}
|
|
123
|
+
*/
|
|
124
|
+
get(labels = {}) {
|
|
125
|
+
const key = this.getLabelKey(labels);
|
|
126
|
+
return (this.values.get(key) || { value: 0 }).value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
toPrometheus() {
|
|
130
|
+
const lines = [];
|
|
131
|
+
lines.push(`# HELP ${this.name} ${this.help}`);
|
|
132
|
+
lines.push(`# TYPE ${this.name} counter`);
|
|
133
|
+
|
|
134
|
+
for (const { value, labels } of this.values.values()) {
|
|
135
|
+
const labelStr = this.formatLabels(labels);
|
|
136
|
+
lines.push(`${this.name}${labelStr} ${value}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.values.size === 0) {
|
|
140
|
+
lines.push(`${this.name} 0`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
toOTLP() {
|
|
147
|
+
const dataPoints = [];
|
|
148
|
+
for (const { value, labels } of this.values.values()) {
|
|
149
|
+
dataPoints.push({
|
|
150
|
+
attributes: Object.entries(labels).map(([k, v]) => ({ key: k, value: { stringValue: String(v) } })),
|
|
151
|
+
asInt: value,
|
|
152
|
+
timeUnixNano: Date.now() * 1e6
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: this.name,
|
|
158
|
+
description: this.help,
|
|
159
|
+
sum: {
|
|
160
|
+
dataPoints,
|
|
161
|
+
aggregationTemporality: 2, // CUMULATIVE
|
|
162
|
+
isMonotonic: true
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// GAUGE
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gauge metric - value that can go up and down
|
|
174
|
+
*/
|
|
175
|
+
class Gauge extends Metric {
|
|
176
|
+
constructor(name, options = {}) {
|
|
177
|
+
super(name, options);
|
|
178
|
+
this.type = MetricType.GAUGE;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Set gauge value
|
|
183
|
+
* @param {number} value - New value
|
|
184
|
+
* @param {Object} labels - Label values
|
|
185
|
+
*/
|
|
186
|
+
set(value, labels = {}) {
|
|
187
|
+
const key = this.getLabelKey(labels);
|
|
188
|
+
this.values.set(key, { value, labels });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Increment gauge
|
|
193
|
+
* @param {number} value - Amount to increment
|
|
194
|
+
* @param {Object} labels - Label values
|
|
195
|
+
*/
|
|
196
|
+
inc(value = 1, labels = {}) {
|
|
197
|
+
const current = this.get(labels);
|
|
198
|
+
this.set(current + value, labels);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Decrement gauge
|
|
203
|
+
* @param {number} value - Amount to decrement
|
|
204
|
+
* @param {Object} labels - Label values
|
|
205
|
+
*/
|
|
206
|
+
dec(value = 1, labels = {}) {
|
|
207
|
+
const current = this.get(labels);
|
|
208
|
+
this.set(current - value, labels);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get current value
|
|
213
|
+
* @param {Object} labels - Label values
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
get(labels = {}) {
|
|
217
|
+
const key = this.getLabelKey(labels);
|
|
218
|
+
return (this.values.get(key) || { value: 0 }).value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Set to current time
|
|
223
|
+
* @param {Object} labels - Label values
|
|
224
|
+
*/
|
|
225
|
+
setToCurrentTime(labels = {}) {
|
|
226
|
+
this.set(Date.now() / 1000, labels);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
toPrometheus() {
|
|
230
|
+
const lines = [];
|
|
231
|
+
lines.push(`# HELP ${this.name} ${this.help}`);
|
|
232
|
+
lines.push(`# TYPE ${this.name} gauge`);
|
|
233
|
+
|
|
234
|
+
for (const { value, labels } of this.values.values()) {
|
|
235
|
+
const labelStr = this.formatLabels(labels);
|
|
236
|
+
lines.push(`${this.name}${labelStr} ${value}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.values.size === 0) {
|
|
240
|
+
lines.push(`${this.name} 0`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
toOTLP() {
|
|
247
|
+
const dataPoints = [];
|
|
248
|
+
for (const { value, labels } of this.values.values()) {
|
|
249
|
+
dataPoints.push({
|
|
250
|
+
attributes: Object.entries(labels).map(([k, v]) => ({ key: k, value: { stringValue: String(v) } })),
|
|
251
|
+
asDouble: value,
|
|
252
|
+
timeUnixNano: Date.now() * 1e6
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
name: this.name,
|
|
258
|
+
description: this.help,
|
|
259
|
+
gauge: { dataPoints }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// HISTOGRAM
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Histogram metric - distribution of values in buckets
|
|
270
|
+
*/
|
|
271
|
+
class Histogram extends Metric {
|
|
272
|
+
constructor(name, options = {}) {
|
|
273
|
+
super(name, options);
|
|
274
|
+
this.type = MetricType.HISTOGRAM;
|
|
275
|
+
this.buckets = options.buckets || [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
|
|
276
|
+
this.buckets.sort((a, b) => a - b);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Observe a value
|
|
281
|
+
* @param {number} value - Value to observe
|
|
282
|
+
* @param {Object} labels - Label values
|
|
283
|
+
*/
|
|
284
|
+
observe(value, labels = {}) {
|
|
285
|
+
const key = this.getLabelKey(labels);
|
|
286
|
+
let entry = this.values.get(key);
|
|
287
|
+
|
|
288
|
+
if (!entry) {
|
|
289
|
+
entry = {
|
|
290
|
+
labels,
|
|
291
|
+
sum: 0,
|
|
292
|
+
count: 0,
|
|
293
|
+
buckets: new Map()
|
|
294
|
+
};
|
|
295
|
+
for (const bucket of this.buckets) {
|
|
296
|
+
entry.buckets.set(bucket, 0);
|
|
297
|
+
}
|
|
298
|
+
entry.buckets.set(Infinity, 0);
|
|
299
|
+
this.values.set(key, entry);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
entry.sum += value;
|
|
303
|
+
entry.count++;
|
|
304
|
+
|
|
305
|
+
for (const bucket of this.buckets) {
|
|
306
|
+
if (value <= bucket) {
|
|
307
|
+
entry.buckets.set(bucket, entry.buckets.get(bucket) + 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
entry.buckets.set(Infinity, entry.buckets.get(Infinity) + 1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Time a function and observe duration
|
|
315
|
+
* @param {Function} fn - Function to time
|
|
316
|
+
* @param {Object} labels - Label values
|
|
317
|
+
* @returns {*} Function result
|
|
318
|
+
*/
|
|
319
|
+
time(fn, labels = {}) {
|
|
320
|
+
const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
321
|
+
try {
|
|
322
|
+
return fn();
|
|
323
|
+
} finally {
|
|
324
|
+
const end = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
325
|
+
const duration = (end - start) / 1000; // Convert to seconds
|
|
326
|
+
this.observe(duration, labels);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Time an async function
|
|
332
|
+
* @param {Function} fn - Async function to time
|
|
333
|
+
* @param {Object} labels - Label values
|
|
334
|
+
* @returns {Promise<*>} Function result
|
|
335
|
+
*/
|
|
336
|
+
async timeAsync(fn, labels = {}) {
|
|
337
|
+
const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
338
|
+
try {
|
|
339
|
+
return await fn();
|
|
340
|
+
} finally {
|
|
341
|
+
const end = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
342
|
+
const duration = (end - start) / 1000;
|
|
343
|
+
this.observe(duration, labels);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
toPrometheus() {
|
|
348
|
+
const lines = [];
|
|
349
|
+
lines.push(`# HELP ${this.name} ${this.help}`);
|
|
350
|
+
lines.push(`# TYPE ${this.name} histogram`);
|
|
351
|
+
|
|
352
|
+
for (const entry of this.values.values()) {
|
|
353
|
+
const labelStr = this.formatLabels(entry.labels);
|
|
354
|
+
const baseName = this.name;
|
|
355
|
+
|
|
356
|
+
// Bucket lines
|
|
357
|
+
for (const bucket of this.buckets) {
|
|
358
|
+
const bucketLabel = labelStr
|
|
359
|
+
? labelStr.slice(0, -1) + `,le="${bucket}"}`
|
|
360
|
+
: `{le="${bucket}"}`;
|
|
361
|
+
lines.push(`${baseName}_bucket${bucketLabel} ${entry.buckets.get(bucket)}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// +Inf bucket
|
|
365
|
+
const infLabel = labelStr
|
|
366
|
+
? labelStr.slice(0, -1) + `,le="+Inf"}`
|
|
367
|
+
: `{le="+Inf"}`;
|
|
368
|
+
lines.push(`${baseName}_bucket${infLabel} ${entry.buckets.get(Infinity)}`);
|
|
369
|
+
|
|
370
|
+
// Sum and count
|
|
371
|
+
lines.push(`${baseName}_sum${labelStr} ${entry.sum}`);
|
|
372
|
+
lines.push(`${baseName}_count${labelStr} ${entry.count}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
toOTLP() {
|
|
379
|
+
const dataPoints = [];
|
|
380
|
+
|
|
381
|
+
for (const entry of this.values.values()) {
|
|
382
|
+
const bucketCounts = [];
|
|
383
|
+
let cumulative = 0;
|
|
384
|
+
|
|
385
|
+
for (const bucket of this.buckets) {
|
|
386
|
+
cumulative += entry.buckets.get(bucket);
|
|
387
|
+
bucketCounts.push(cumulative);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
dataPoints.push({
|
|
391
|
+
attributes: Object.entries(entry.labels).map(([k, v]) => ({ key: k, value: { stringValue: String(v) } })),
|
|
392
|
+
count: entry.count,
|
|
393
|
+
sum: entry.sum,
|
|
394
|
+
bucketCounts,
|
|
395
|
+
explicitBounds: this.buckets,
|
|
396
|
+
timeUnixNano: Date.now() * 1e6
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
name: this.name,
|
|
402
|
+
description: this.help,
|
|
403
|
+
histogram: {
|
|
404
|
+
dataPoints,
|
|
405
|
+
aggregationTemporality: 2 // CUMULATIVE
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// SUMMARY
|
|
413
|
+
// ============================================================================
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Summary metric - quantile distribution
|
|
417
|
+
*/
|
|
418
|
+
class Summary extends Metric {
|
|
419
|
+
constructor(name, options = {}) {
|
|
420
|
+
super(name, options);
|
|
421
|
+
this.type = MetricType.SUMMARY;
|
|
422
|
+
this.quantiles = options.quantiles || [0.5, 0.9, 0.99];
|
|
423
|
+
this.maxAge = options.maxAge || 600000; // 10 minutes
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Observe a value
|
|
428
|
+
* @param {number} value - Value to observe
|
|
429
|
+
* @param {Object} labels - Label values
|
|
430
|
+
*/
|
|
431
|
+
observe(value, labels = {}) {
|
|
432
|
+
const key = this.getLabelKey(labels);
|
|
433
|
+
let entry = this.values.get(key);
|
|
434
|
+
|
|
435
|
+
if (!entry) {
|
|
436
|
+
entry = {
|
|
437
|
+
labels,
|
|
438
|
+
sum: 0,
|
|
439
|
+
count: 0,
|
|
440
|
+
samples: []
|
|
441
|
+
};
|
|
442
|
+
this.values.set(key, entry);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
entry.sum += value;
|
|
446
|
+
entry.count++;
|
|
447
|
+
entry.samples.push({ value, timestamp: Date.now() });
|
|
448
|
+
|
|
449
|
+
// Age out old samples
|
|
450
|
+
const cutoff = Date.now() - this.maxAge;
|
|
451
|
+
entry.samples = entry.samples.filter(s => s.timestamp > cutoff);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Calculate quantile value
|
|
456
|
+
* @param {Array} samples - Samples
|
|
457
|
+
* @param {number} q - Quantile (0-1)
|
|
458
|
+
* @returns {number}
|
|
459
|
+
*/
|
|
460
|
+
calculateQuantile(samples, q) {
|
|
461
|
+
if (samples.length === 0) return 0;
|
|
462
|
+
|
|
463
|
+
const sorted = samples.map(s => s.value).sort((a, b) => a - b);
|
|
464
|
+
const pos = q * (sorted.length - 1);
|
|
465
|
+
const lower = Math.floor(pos);
|
|
466
|
+
const upper = Math.ceil(pos);
|
|
467
|
+
|
|
468
|
+
if (lower === upper) return sorted[lower];
|
|
469
|
+
|
|
470
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (pos - lower);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
toPrometheus() {
|
|
474
|
+
const lines = [];
|
|
475
|
+
lines.push(`# HELP ${this.name} ${this.help}`);
|
|
476
|
+
lines.push(`# TYPE ${this.name} summary`);
|
|
477
|
+
|
|
478
|
+
for (const entry of this.values.values()) {
|
|
479
|
+
const labelStr = this.formatLabels(entry.labels);
|
|
480
|
+
|
|
481
|
+
// Quantile lines
|
|
482
|
+
for (const q of this.quantiles) {
|
|
483
|
+
const qValue = this.calculateQuantile(entry.samples, q);
|
|
484
|
+
const qLabel = labelStr
|
|
485
|
+
? labelStr.slice(0, -1) + `,quantile="${q}"}`
|
|
486
|
+
: `{quantile="${q}"}`;
|
|
487
|
+
lines.push(`${this.name}${qLabel} ${qValue}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Sum and count
|
|
491
|
+
lines.push(`${this.name}_sum${labelStr} ${entry.sum}`);
|
|
492
|
+
lines.push(`${this.name}_count${labelStr} ${entry.count}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return lines.join('\n');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
toOTLP() {
|
|
499
|
+
const dataPoints = [];
|
|
500
|
+
|
|
501
|
+
for (const entry of this.values.values()) {
|
|
502
|
+
const quantileValues = this.quantiles.map(q => ({
|
|
503
|
+
quantile: q,
|
|
504
|
+
value: this.calculateQuantile(entry.samples, q)
|
|
505
|
+
}));
|
|
506
|
+
|
|
507
|
+
dataPoints.push({
|
|
508
|
+
attributes: Object.entries(entry.labels).map(([k, v]) => ({ key: k, value: { stringValue: String(v) } })),
|
|
509
|
+
count: entry.count,
|
|
510
|
+
sum: entry.sum,
|
|
511
|
+
quantileValues,
|
|
512
|
+
timeUnixNano: Date.now() * 1e6
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
name: this.name,
|
|
518
|
+
description: this.help,
|
|
519
|
+
summary: { dataPoints }
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// METRIC REGISTRY
|
|
526
|
+
// ============================================================================
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* MetricRegistry - manages all metrics
|
|
530
|
+
*/
|
|
531
|
+
class MetricRegistry extends SimpleEventEmitter {
|
|
532
|
+
constructor(options = {}) {
|
|
533
|
+
super();
|
|
534
|
+
this.prefix = options.prefix || '';
|
|
535
|
+
this.metrics = new Map();
|
|
536
|
+
this.defaultLabels = options.defaultLabels || {};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get prefixed metric name
|
|
541
|
+
* @param {string} name - Metric name
|
|
542
|
+
* @returns {string}
|
|
543
|
+
*/
|
|
544
|
+
getFullName(name) {
|
|
545
|
+
return this.prefix + name;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Create and register a counter
|
|
550
|
+
* @param {string} name - Metric name
|
|
551
|
+
* @param {Object} options - Metric options
|
|
552
|
+
* @returns {Counter}
|
|
553
|
+
*/
|
|
554
|
+
counter(name, options = {}) {
|
|
555
|
+
const fullName = this.getFullName(name);
|
|
556
|
+
if (this.metrics.has(fullName)) {
|
|
557
|
+
return this.metrics.get(fullName);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const metric = new Counter(fullName, options);
|
|
561
|
+
this.metrics.set(fullName, metric);
|
|
562
|
+
return metric;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create and register a gauge
|
|
567
|
+
* @param {string} name - Metric name
|
|
568
|
+
* @param {Object} options - Metric options
|
|
569
|
+
* @returns {Gauge}
|
|
570
|
+
*/
|
|
571
|
+
gauge(name, options = {}) {
|
|
572
|
+
const fullName = this.getFullName(name);
|
|
573
|
+
if (this.metrics.has(fullName)) {
|
|
574
|
+
return this.metrics.get(fullName);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const metric = new Gauge(fullName, options);
|
|
578
|
+
this.metrics.set(fullName, metric);
|
|
579
|
+
return metric;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Create and register a histogram
|
|
584
|
+
* @param {string} name - Metric name
|
|
585
|
+
* @param {Object} options - Metric options
|
|
586
|
+
* @returns {Histogram}
|
|
587
|
+
*/
|
|
588
|
+
histogram(name, options = {}) {
|
|
589
|
+
const fullName = this.getFullName(name);
|
|
590
|
+
if (this.metrics.has(fullName)) {
|
|
591
|
+
return this.metrics.get(fullName);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const metric = new Histogram(fullName, options);
|
|
595
|
+
this.metrics.set(fullName, metric);
|
|
596
|
+
return metric;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Create and register a summary
|
|
601
|
+
* @param {string} name - Metric name
|
|
602
|
+
* @param {Object} options - Metric options
|
|
603
|
+
* @returns {Summary}
|
|
604
|
+
*/
|
|
605
|
+
summary(name, options = {}) {
|
|
606
|
+
const fullName = this.getFullName(name);
|
|
607
|
+
if (this.metrics.has(fullName)) {
|
|
608
|
+
return this.metrics.get(fullName);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const metric = new Summary(fullName, options);
|
|
612
|
+
this.metrics.set(fullName, metric);
|
|
613
|
+
return metric;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Get a metric by name
|
|
618
|
+
* @param {string} name - Metric name
|
|
619
|
+
* @returns {Metric}
|
|
620
|
+
*/
|
|
621
|
+
get(name) {
|
|
622
|
+
return this.metrics.get(this.getFullName(name));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Remove a metric
|
|
627
|
+
* @param {string} name - Metric name
|
|
628
|
+
*/
|
|
629
|
+
remove(name) {
|
|
630
|
+
this.metrics.delete(this.getFullName(name));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Clear all metrics
|
|
635
|
+
*/
|
|
636
|
+
clear() {
|
|
637
|
+
this.metrics.clear();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Export all metrics to Prometheus format
|
|
642
|
+
* @returns {string}
|
|
643
|
+
*/
|
|
644
|
+
toPrometheus() {
|
|
645
|
+
const lines = [];
|
|
646
|
+
|
|
647
|
+
for (const metric of this.metrics.values()) {
|
|
648
|
+
lines.push(metric.toPrometheus());
|
|
649
|
+
lines.push('');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return lines.join('\n');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Export all metrics to OpenTelemetry format
|
|
657
|
+
* @returns {Object}
|
|
658
|
+
*/
|
|
659
|
+
toOTLP() {
|
|
660
|
+
const metrics = [];
|
|
661
|
+
|
|
662
|
+
for (const metric of this.metrics.values()) {
|
|
663
|
+
metrics.push(metric.toOTLP());
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
resourceMetrics: [{
|
|
668
|
+
resource: {
|
|
669
|
+
attributes: Object.entries(this.defaultLabels).map(([k, v]) => ({
|
|
670
|
+
key: k,
|
|
671
|
+
value: { stringValue: String(v) }
|
|
672
|
+
}))
|
|
673
|
+
},
|
|
674
|
+
scopeMetrics: [{
|
|
675
|
+
scope: { name: 'tinyaleph' },
|
|
676
|
+
metrics
|
|
677
|
+
}]
|
|
678
|
+
}]
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get all metric names
|
|
684
|
+
* @returns {Array<string>}
|
|
685
|
+
*/
|
|
686
|
+
getMetricNames() {
|
|
687
|
+
return Array.from(this.metrics.keys());
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// EXPORTS
|
|
693
|
+
// ============================================================================
|
|
694
|
+
|
|
695
|
+
module.exports = {
|
|
696
|
+
// Metric types
|
|
697
|
+
MetricType,
|
|
698
|
+
|
|
699
|
+
// Metric classes
|
|
700
|
+
Metric,
|
|
701
|
+
Counter,
|
|
702
|
+
Gauge,
|
|
703
|
+
Histogram,
|
|
704
|
+
Summary,
|
|
705
|
+
|
|
706
|
+
// Registry
|
|
707
|
+
MetricRegistry
|
|
708
|
+
};
|