@sentienguard/apm 1.0.5 → 1.0.6
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 +1 -1
- package/src/aggregator.js +463 -463
- package/src/browser/transport.js +22 -21
- package/src/browser.js +3 -2
- package/src/circuitBreaker.js +264 -254
- package/src/dependencies.js +231 -236
- package/src/index.js +209 -209
- package/src/instrumentation.js +208 -208
- package/src/normalizer.js +147 -147
- package/src/transport.js +215 -214
package/src/aggregator.js
CHANGED
|
@@ -1,463 +1,463 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Metrics Aggregator
|
|
3
|
-
* Aggregates metrics in memory per interval.
|
|
4
|
-
* Never streams raw events - only sends aggregated data.
|
|
5
|
-
*
|
|
6
|
-
* Aggregation key: (service, method, route) for requests
|
|
7
|
-
* Aggregation key: (service, name, type) for dependencies
|
|
8
|
-
* Aggregation key: (collection, operation) for MongoDB operations
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { RouteRegistry } from './normalizer.js';
|
|
12
|
-
import config from './config.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Create an aggregation key for request metrics
|
|
16
|
-
*/
|
|
17
|
-
function createRequestKey(method, route) {
|
|
18
|
-
return `${method}:${route}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create an aggregation key for dependency metrics
|
|
23
|
-
*/
|
|
24
|
-
function createDependencyKey(name, type) {
|
|
25
|
-
return `${name}:${type}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create an aggregation key for MongoDB operation metrics
|
|
30
|
-
*/
|
|
31
|
-
function createMongoKey(collection, operation) {
|
|
32
|
-
return `${collection}:${operation}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create an aggregation key for OpenAI operation metrics
|
|
37
|
-
*/
|
|
38
|
-
function createOpenAIKey(operation, model) {
|
|
39
|
-
return `${operation}:${model}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Metrics Aggregator class
|
|
44
|
-
* Maintains in-memory aggregated metrics that are flushed periodically
|
|
45
|
-
*/
|
|
46
|
-
export class MetricsAggregator {
|
|
47
|
-
constructor() {
|
|
48
|
-
this.routeRegistry = new RouteRegistry(config.maxRoutes);
|
|
49
|
-
this.reset();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Reset all aggregated metrics (called after flush)
|
|
54
|
-
*/
|
|
55
|
-
reset() {
|
|
56
|
-
// Request metrics: Map<key, {count, errorCount, latency: {sum, min, max}}>
|
|
57
|
-
this.requests = new Map();
|
|
58
|
-
|
|
59
|
-
// Dependency metrics: Map<key, {name, type, count, errorCount, latency: {sum, min, max}}>
|
|
60
|
-
this.dependencies = new Map();
|
|
61
|
-
|
|
62
|
-
// MongoDB operation metrics: Map<key, {collection, operation, count, errorCount, latency: {sum, min, max}}>
|
|
63
|
-
this.mongodbOperations = new Map();
|
|
64
|
-
|
|
65
|
-
// MongoDB slow queries: Array<{collection, operation, latency, timestamp}>
|
|
66
|
-
this.slowQueries = [];
|
|
67
|
-
|
|
68
|
-
// OpenAI operation metrics: Map<key, {operation, model, count, errorCount, latency, tokens, cost}>
|
|
69
|
-
this.openaiOperations = new Map();
|
|
70
|
-
|
|
71
|
-
// MongoDB connection pool stats (updated periodically, not reset)
|
|
72
|
-
if (!this.mongodbPoolStats) {
|
|
73
|
-
this.mongodbPoolStats = {
|
|
74
|
-
active: 0,
|
|
75
|
-
idle: 0,
|
|
76
|
-
waitQueueSize: 0,
|
|
77
|
-
lastUpdated: null
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Circuit breaker state changes: Array<{service, state, timestamp}>
|
|
82
|
-
this.circuitBreakerStates = [];
|
|
83
|
-
|
|
84
|
-
// Error counter for unhandled exceptions
|
|
85
|
-
this.unhandledErrors = 0;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Record an incoming HTTP request
|
|
90
|
-
*
|
|
91
|
-
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
92
|
-
* @param {string} route - Normalized route path
|
|
93
|
-
* @param {number} latency - Response time in milliseconds
|
|
94
|
-
* @param {boolean} isError - Whether the request resulted in an error (4xx/5xx)
|
|
95
|
-
*/
|
|
96
|
-
recordRequest(method, route, latency, isError = false) {
|
|
97
|
-
// Register route with limit enforcement
|
|
98
|
-
const registeredRoute = this.routeRegistry.register(route);
|
|
99
|
-
const key = createRequestKey(method.toUpperCase(), registeredRoute);
|
|
100
|
-
|
|
101
|
-
let metric = this.requests.get(key);
|
|
102
|
-
if (!metric) {
|
|
103
|
-
metric = {
|
|
104
|
-
method: method.toUpperCase(),
|
|
105
|
-
route: registeredRoute,
|
|
106
|
-
count: 0,
|
|
107
|
-
errorCount: 0,
|
|
108
|
-
latency: {
|
|
109
|
-
sum: 0,
|
|
110
|
-
min: Infinity,
|
|
111
|
-
max: 0
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
this.requests.set(key, metric);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
metric.count++;
|
|
118
|
-
if (isError) {
|
|
119
|
-
metric.errorCount++;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
metric.latency.sum += latency;
|
|
123
|
-
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
124
|
-
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Record an outgoing dependency call
|
|
129
|
-
*
|
|
130
|
-
* @param {string} name - Dependency name (e.g., "OpenAI API", "MongoDB")
|
|
131
|
-
* @param {string} type - Dependency type (http, db, cache)
|
|
132
|
-
* @param {number} latency - Response time in milliseconds
|
|
133
|
-
* @param {boolean} isError - Whether the call resulted in an error
|
|
134
|
-
*/
|
|
135
|
-
recordDependency(name, type, latency, isError = false) {
|
|
136
|
-
const key = createDependencyKey(name, type);
|
|
137
|
-
|
|
138
|
-
let metric = this.dependencies.get(key);
|
|
139
|
-
if (!metric) {
|
|
140
|
-
metric = {
|
|
141
|
-
name,
|
|
142
|
-
type,
|
|
143
|
-
count: 0,
|
|
144
|
-
errorCount: 0,
|
|
145
|
-
latency: {
|
|
146
|
-
sum: 0,
|
|
147
|
-
min: Infinity,
|
|
148
|
-
max: 0
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
this.dependencies.set(key, metric);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
metric.count++;
|
|
155
|
-
if (isError) {
|
|
156
|
-
metric.errorCount++;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
metric.latency.sum += latency;
|
|
160
|
-
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
161
|
-
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Record an unhandled error
|
|
166
|
-
*/
|
|
167
|
-
recordError() {
|
|
168
|
-
this.unhandledErrors++;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Record a MongoDB operation
|
|
173
|
-
*
|
|
174
|
-
* @param {string} collection - Collection name
|
|
175
|
-
* @param {string} operation - Operation type (find, insert, update, delete, aggregate)
|
|
176
|
-
* @param {number} latency - Response time in milliseconds
|
|
177
|
-
* @param {boolean} isError - Whether the operation resulted in an error
|
|
178
|
-
*/
|
|
179
|
-
recordMongoOperation(collection, operation, latency, isError = false) {
|
|
180
|
-
const key = createMongoKey(collection, operation);
|
|
181
|
-
|
|
182
|
-
let metric = this.mongodbOperations.get(key);
|
|
183
|
-
if (!metric) {
|
|
184
|
-
metric = {
|
|
185
|
-
collection,
|
|
186
|
-
operation,
|
|
187
|
-
count: 0,
|
|
188
|
-
errorCount: 0,
|
|
189
|
-
latency: {
|
|
190
|
-
sum: 0,
|
|
191
|
-
min: Infinity,
|
|
192
|
-
max: 0
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
this.mongodbOperations.set(key, metric);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
metric.count++;
|
|
199
|
-
if (isError) {
|
|
200
|
-
metric.errorCount++;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
metric.latency.sum += latency;
|
|
204
|
-
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
205
|
-
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Record a slow MongoDB query
|
|
210
|
-
*
|
|
211
|
-
* @param {string} collection - Collection name
|
|
212
|
-
* @param {string} operation - Operation type
|
|
213
|
-
* @param {number} latency - Response time in milliseconds
|
|
214
|
-
*/
|
|
215
|
-
recordSlowQuery(collection, operation, latency) {
|
|
216
|
-
// Keep only the most recent slow queries (max 100 per interval)
|
|
217
|
-
if (this.slowQueries.length >= 100) {
|
|
218
|
-
this.slowQueries.shift();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.slowQueries.push({
|
|
222
|
-
collection,
|
|
223
|
-
operation,
|
|
224
|
-
latency: Math.round(latency),
|
|
225
|
-
timestamp: Date.now()
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Record an OpenAI API operation
|
|
231
|
-
*
|
|
232
|
-
* @param {Object} data - Operation data
|
|
233
|
-
* @param {string} data.operation - Operation type (chat.completions, embeddings, images.generate, etc.)
|
|
234
|
-
* @param {string} data.model - Model used (gpt-4, gpt-3.5-turbo, etc.)
|
|
235
|
-
* @param {number} data.latency - Response time in milliseconds
|
|
236
|
-
* @param {number} data.promptTokens - Number of prompt tokens used
|
|
237
|
-
* @param {number} data.completionTokens - Number of completion tokens used
|
|
238
|
-
* @param {number} data.totalTokens - Total tokens used
|
|
239
|
-
* @param {number} data.cost - Estimated cost in USD
|
|
240
|
-
* @param {string|null} data.error - Error message if failed
|
|
241
|
-
* @param {number} data.statusCode - HTTP status code
|
|
242
|
-
*/
|
|
243
|
-
recordOpenAIOperation(data) {
|
|
244
|
-
const key = createOpenAIKey(data.operation, data.model);
|
|
245
|
-
|
|
246
|
-
let metric = this.openaiOperations.get(key);
|
|
247
|
-
if (!metric) {
|
|
248
|
-
metric = {
|
|
249
|
-
operation: data.operation,
|
|
250
|
-
model: data.model,
|
|
251
|
-
count: 0,
|
|
252
|
-
errorCount: 0,
|
|
253
|
-
latency: {
|
|
254
|
-
sum: 0,
|
|
255
|
-
min: Infinity,
|
|
256
|
-
max: 0
|
|
257
|
-
},
|
|
258
|
-
tokens: {
|
|
259
|
-
prompt: 0,
|
|
260
|
-
completion: 0,
|
|
261
|
-
total: 0
|
|
262
|
-
},
|
|
263
|
-
cost: 0
|
|
264
|
-
};
|
|
265
|
-
this.openaiOperations.set(key, metric);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
metric.count++;
|
|
269
|
-
if (data.error) {
|
|
270
|
-
metric.errorCount++;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
metric.latency.sum += data.latency;
|
|
274
|
-
metric.latency.min = Math.min(metric.latency.min, data.latency);
|
|
275
|
-
metric.latency.max = Math.max(metric.latency.max, data.latency);
|
|
276
|
-
|
|
277
|
-
metric.tokens.prompt += data.promptTokens || 0;
|
|
278
|
-
metric.tokens.completion += data.completionTokens || 0;
|
|
279
|
-
metric.tokens.total += data.totalTokens || 0;
|
|
280
|
-
|
|
281
|
-
metric.cost += data.cost || 0;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Update MongoDB connection pool statistics
|
|
286
|
-
*
|
|
287
|
-
* @param {Object} stats - Pool statistics
|
|
288
|
-
* @param {number} stats.active - Active connections (checked out)
|
|
289
|
-
* @param {number} stats.idle - Idle connections (available in pool)
|
|
290
|
-
* @param {number} stats.waitQueueSize - Wait queue size
|
|
291
|
-
* @param {number} stats.total - Total connections in pool
|
|
292
|
-
*/
|
|
293
|
-
updatePoolStats(stats) {
|
|
294
|
-
this.mongodbPoolStats = {
|
|
295
|
-
active: stats.active || 0,
|
|
296
|
-
idle: stats.idle || 0,
|
|
297
|
-
waitQueueSize: stats.waitQueueSize || 0,
|
|
298
|
-
total: stats.total || (stats.active || 0) + (stats.idle || 0),
|
|
299
|
-
lastUpdated: Date.now()
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Record a circuit breaker state change
|
|
305
|
-
*
|
|
306
|
-
* @param {string} service - Service/dependency name (e.g., 'mongodb')
|
|
307
|
-
* @param {string} state - New state ('open', 'halfOpen', 'close')
|
|
308
|
-
*/
|
|
309
|
-
recordCircuitState(service, state) {
|
|
310
|
-
this.circuitBreakerStates.push({
|
|
311
|
-
service,
|
|
312
|
-
state,
|
|
313
|
-
timestamp: Date.now()
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Check if there's data to flush
|
|
319
|
-
*/
|
|
320
|
-
hasData() {
|
|
321
|
-
return this.requests.size > 0 ||
|
|
322
|
-
this.dependencies.size > 0 ||
|
|
323
|
-
this.mongodbOperations.size > 0 ||
|
|
324
|
-
this.slowQueries.length > 0 ||
|
|
325
|
-
this.circuitBreakerStates.length > 0 ||
|
|
326
|
-
this.openaiOperations.size > 0;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Get aggregated data for flushing to backend
|
|
331
|
-
* Returns the payload in the expected format and resets counters
|
|
332
|
-
*/
|
|
333
|
-
flush() {
|
|
334
|
-
const payload = {
|
|
335
|
-
interval: `${config.flushInterval}s`,
|
|
336
|
-
service: config.service,
|
|
337
|
-
environment: config.environment,
|
|
338
|
-
requests: [],
|
|
339
|
-
dependencies: [],
|
|
340
|
-
mongodb: [],
|
|
341
|
-
mongodbPool: null,
|
|
342
|
-
slowQueries: [],
|
|
343
|
-
circuitBreaker: [],
|
|
344
|
-
openai: []
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// Convert request metrics to array
|
|
348
|
-
for (const metric of this.requests.values()) {
|
|
349
|
-
payload.requests.push({
|
|
350
|
-
method: metric.method,
|
|
351
|
-
route: metric.route,
|
|
352
|
-
count: metric.count,
|
|
353
|
-
errorCount: metric.errorCount,
|
|
354
|
-
latency: {
|
|
355
|
-
sum: Math.round(metric.latency.sum),
|
|
356
|
-
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
357
|
-
max: Math.round(metric.latency.max)
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Convert dependency metrics to array
|
|
363
|
-
for (const metric of this.dependencies.values()) {
|
|
364
|
-
payload.dependencies.push({
|
|
365
|
-
name: metric.name,
|
|
366
|
-
type: metric.type,
|
|
367
|
-
count: metric.count,
|
|
368
|
-
errorCount: metric.errorCount,
|
|
369
|
-
latency: {
|
|
370
|
-
sum: Math.round(metric.latency.sum),
|
|
371
|
-
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
372
|
-
max: Math.round(metric.latency.max)
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Convert MongoDB operation metrics to array
|
|
378
|
-
for (const metric of this.mongodbOperations.values()) {
|
|
379
|
-
payload.mongodb.push({
|
|
380
|
-
collection: metric.collection,
|
|
381
|
-
operation: metric.operation,
|
|
382
|
-
count: metric.count,
|
|
383
|
-
errorCount: metric.errorCount,
|
|
384
|
-
latency: {
|
|
385
|
-
sum: Math.round(metric.latency.sum),
|
|
386
|
-
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
387
|
-
max: Math.round(metric.latency.max)
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Include MongoDB pool stats if available
|
|
393
|
-
if (this.mongodbPoolStats
|
|
394
|
-
payload.mongodbPool = { ...this.mongodbPoolStats };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Include slow queries
|
|
398
|
-
payload.slowQueries = [...this.slowQueries];
|
|
399
|
-
|
|
400
|
-
// Include circuit breaker state changes
|
|
401
|
-
payload.circuitBreaker = [...this.circuitBreakerStates];
|
|
402
|
-
|
|
403
|
-
// Convert OpenAI operation metrics to array
|
|
404
|
-
for (const metric of this.openaiOperations.values()) {
|
|
405
|
-
payload.openai.push({
|
|
406
|
-
operation: metric.operation,
|
|
407
|
-
model: metric.model,
|
|
408
|
-
count: metric.count,
|
|
409
|
-
errorCount: metric.errorCount,
|
|
410
|
-
latency: {
|
|
411
|
-
sum: Math.round(metric.latency.sum),
|
|
412
|
-
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
413
|
-
max: Math.round(metric.latency.max)
|
|
414
|
-
},
|
|
415
|
-
tokens: {
|
|
416
|
-
prompt: metric.tokens.prompt,
|
|
417
|
-
completion: metric.tokens.completion,
|
|
418
|
-
total: metric.tokens.total
|
|
419
|
-
},
|
|
420
|
-
cost: Math.round(metric.cost * 1000000) / 1000000 // 6 decimal places
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Reset for next interval
|
|
425
|
-
this.reset();
|
|
426
|
-
|
|
427
|
-
return payload;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Get current metrics count (for monitoring/testing)
|
|
432
|
-
*/
|
|
433
|
-
getStats() {
|
|
434
|
-
return {
|
|
435
|
-
requestMetrics: this.requests.size,
|
|
436
|
-
dependencyMetrics: this.dependencies.size,
|
|
437
|
-
mongodbMetrics: this.mongodbOperations.size,
|
|
438
|
-
openaiMetrics: this.openaiOperations.size,
|
|
439
|
-
slowQueries: this.slowQueries.length,
|
|
440
|
-
circuitBreakerEvents: this.circuitBreakerStates.length,
|
|
441
|
-
uniqueRoutes: this.routeRegistry.size,
|
|
442
|
-
unhandledErrors: this.unhandledErrors
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Singleton instance
|
|
448
|
-
let instance = null;
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Get the singleton aggregator instance
|
|
452
|
-
*/
|
|
453
|
-
export function getAggregator() {
|
|
454
|
-
if (!instance) {
|
|
455
|
-
instance = new MetricsAggregator();
|
|
456
|
-
}
|
|
457
|
-
return instance;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export default {
|
|
461
|
-
MetricsAggregator,
|
|
462
|
-
getAggregator
|
|
463
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Aggregator
|
|
3
|
+
* Aggregates metrics in memory per interval.
|
|
4
|
+
* Never streams raw events - only sends aggregated data.
|
|
5
|
+
*
|
|
6
|
+
* Aggregation key: (service, method, route) for requests
|
|
7
|
+
* Aggregation key: (service, name, type) for dependencies
|
|
8
|
+
* Aggregation key: (collection, operation) for MongoDB operations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { RouteRegistry } from './normalizer.js';
|
|
12
|
+
import config from './config.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an aggregation key for request metrics
|
|
16
|
+
*/
|
|
17
|
+
function createRequestKey(method, route) {
|
|
18
|
+
return `${method}:${route}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create an aggregation key for dependency metrics
|
|
23
|
+
*/
|
|
24
|
+
function createDependencyKey(name, type) {
|
|
25
|
+
return `${name}:${type}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create an aggregation key for MongoDB operation metrics
|
|
30
|
+
*/
|
|
31
|
+
function createMongoKey(collection, operation) {
|
|
32
|
+
return `${collection}:${operation}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create an aggregation key for OpenAI operation metrics
|
|
37
|
+
*/
|
|
38
|
+
function createOpenAIKey(operation, model) {
|
|
39
|
+
return `${operation}:${model}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Metrics Aggregator class
|
|
44
|
+
* Maintains in-memory aggregated metrics that are flushed periodically
|
|
45
|
+
*/
|
|
46
|
+
export class MetricsAggregator {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.routeRegistry = new RouteRegistry(config.maxRoutes);
|
|
49
|
+
this.reset();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reset all aggregated metrics (called after flush)
|
|
54
|
+
*/
|
|
55
|
+
reset() {
|
|
56
|
+
// Request metrics: Map<key, {count, errorCount, latency: {sum, min, max}}>
|
|
57
|
+
this.requests = new Map();
|
|
58
|
+
|
|
59
|
+
// Dependency metrics: Map<key, {name, type, count, errorCount, latency: {sum, min, max}}>
|
|
60
|
+
this.dependencies = new Map();
|
|
61
|
+
|
|
62
|
+
// MongoDB operation metrics: Map<key, {collection, operation, count, errorCount, latency: {sum, min, max}}>
|
|
63
|
+
this.mongodbOperations = new Map();
|
|
64
|
+
|
|
65
|
+
// MongoDB slow queries: Array<{collection, operation, latency, timestamp}>
|
|
66
|
+
this.slowQueries = [];
|
|
67
|
+
|
|
68
|
+
// OpenAI operation metrics: Map<key, {operation, model, count, errorCount, latency, tokens, cost}>
|
|
69
|
+
this.openaiOperations = new Map();
|
|
70
|
+
|
|
71
|
+
// MongoDB connection pool stats (updated periodically, not reset)
|
|
72
|
+
if (!this.mongodbPoolStats) {
|
|
73
|
+
this.mongodbPoolStats = {
|
|
74
|
+
active: 0,
|
|
75
|
+
idle: 0,
|
|
76
|
+
waitQueueSize: 0,
|
|
77
|
+
lastUpdated: null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Circuit breaker state changes: Array<{service, state, timestamp}>
|
|
82
|
+
this.circuitBreakerStates = [];
|
|
83
|
+
|
|
84
|
+
// Error counter for unhandled exceptions
|
|
85
|
+
this.unhandledErrors = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Record an incoming HTTP request
|
|
90
|
+
*
|
|
91
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
92
|
+
* @param {string} route - Normalized route path
|
|
93
|
+
* @param {number} latency - Response time in milliseconds
|
|
94
|
+
* @param {boolean} isError - Whether the request resulted in an error (4xx/5xx)
|
|
95
|
+
*/
|
|
96
|
+
recordRequest(method, route, latency, isError = false) {
|
|
97
|
+
// Register route with limit enforcement
|
|
98
|
+
const registeredRoute = this.routeRegistry.register(route);
|
|
99
|
+
const key = createRequestKey(method.toUpperCase(), registeredRoute);
|
|
100
|
+
|
|
101
|
+
let metric = this.requests.get(key);
|
|
102
|
+
if (!metric) {
|
|
103
|
+
metric = {
|
|
104
|
+
method: method.toUpperCase(),
|
|
105
|
+
route: registeredRoute,
|
|
106
|
+
count: 0,
|
|
107
|
+
errorCount: 0,
|
|
108
|
+
latency: {
|
|
109
|
+
sum: 0,
|
|
110
|
+
min: Infinity,
|
|
111
|
+
max: 0
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
this.requests.set(key, metric);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
metric.count++;
|
|
118
|
+
if (isError) {
|
|
119
|
+
metric.errorCount++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
metric.latency.sum += latency;
|
|
123
|
+
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
124
|
+
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Record an outgoing dependency call
|
|
129
|
+
*
|
|
130
|
+
* @param {string} name - Dependency name (e.g., "OpenAI API", "MongoDB")
|
|
131
|
+
* @param {string} type - Dependency type (http, db, cache)
|
|
132
|
+
* @param {number} latency - Response time in milliseconds
|
|
133
|
+
* @param {boolean} isError - Whether the call resulted in an error
|
|
134
|
+
*/
|
|
135
|
+
recordDependency(name, type, latency, isError = false) {
|
|
136
|
+
const key = createDependencyKey(name, type);
|
|
137
|
+
|
|
138
|
+
let metric = this.dependencies.get(key);
|
|
139
|
+
if (!metric) {
|
|
140
|
+
metric = {
|
|
141
|
+
name,
|
|
142
|
+
type,
|
|
143
|
+
count: 0,
|
|
144
|
+
errorCount: 0,
|
|
145
|
+
latency: {
|
|
146
|
+
sum: 0,
|
|
147
|
+
min: Infinity,
|
|
148
|
+
max: 0
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
this.dependencies.set(key, metric);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
metric.count++;
|
|
155
|
+
if (isError) {
|
|
156
|
+
metric.errorCount++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
metric.latency.sum += latency;
|
|
160
|
+
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
161
|
+
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Record an unhandled error
|
|
166
|
+
*/
|
|
167
|
+
recordError() {
|
|
168
|
+
this.unhandledErrors++;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Record a MongoDB operation
|
|
173
|
+
*
|
|
174
|
+
* @param {string} collection - Collection name
|
|
175
|
+
* @param {string} operation - Operation type (find, insert, update, delete, aggregate)
|
|
176
|
+
* @param {number} latency - Response time in milliseconds
|
|
177
|
+
* @param {boolean} isError - Whether the operation resulted in an error
|
|
178
|
+
*/
|
|
179
|
+
recordMongoOperation(collection, operation, latency, isError = false) {
|
|
180
|
+
const key = createMongoKey(collection, operation);
|
|
181
|
+
|
|
182
|
+
let metric = this.mongodbOperations.get(key);
|
|
183
|
+
if (!metric) {
|
|
184
|
+
metric = {
|
|
185
|
+
collection,
|
|
186
|
+
operation,
|
|
187
|
+
count: 0,
|
|
188
|
+
errorCount: 0,
|
|
189
|
+
latency: {
|
|
190
|
+
sum: 0,
|
|
191
|
+
min: Infinity,
|
|
192
|
+
max: 0
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
this.mongodbOperations.set(key, metric);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
metric.count++;
|
|
199
|
+
if (isError) {
|
|
200
|
+
metric.errorCount++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
metric.latency.sum += latency;
|
|
204
|
+
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
205
|
+
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Record a slow MongoDB query
|
|
210
|
+
*
|
|
211
|
+
* @param {string} collection - Collection name
|
|
212
|
+
* @param {string} operation - Operation type
|
|
213
|
+
* @param {number} latency - Response time in milliseconds
|
|
214
|
+
*/
|
|
215
|
+
recordSlowQuery(collection, operation, latency) {
|
|
216
|
+
// Keep only the most recent slow queries (max 100 per interval)
|
|
217
|
+
if (this.slowQueries.length >= 100) {
|
|
218
|
+
this.slowQueries.shift();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.slowQueries.push({
|
|
222
|
+
collection,
|
|
223
|
+
operation,
|
|
224
|
+
latency: Math.round(latency),
|
|
225
|
+
timestamp: Date.now()
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Record an OpenAI API operation
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} data - Operation data
|
|
233
|
+
* @param {string} data.operation - Operation type (chat.completions, embeddings, images.generate, etc.)
|
|
234
|
+
* @param {string} data.model - Model used (gpt-4, gpt-3.5-turbo, etc.)
|
|
235
|
+
* @param {number} data.latency - Response time in milliseconds
|
|
236
|
+
* @param {number} data.promptTokens - Number of prompt tokens used
|
|
237
|
+
* @param {number} data.completionTokens - Number of completion tokens used
|
|
238
|
+
* @param {number} data.totalTokens - Total tokens used
|
|
239
|
+
* @param {number} data.cost - Estimated cost in USD
|
|
240
|
+
* @param {string|null} data.error - Error message if failed
|
|
241
|
+
* @param {number} data.statusCode - HTTP status code
|
|
242
|
+
*/
|
|
243
|
+
recordOpenAIOperation(data) {
|
|
244
|
+
const key = createOpenAIKey(data.operation, data.model);
|
|
245
|
+
|
|
246
|
+
let metric = this.openaiOperations.get(key);
|
|
247
|
+
if (!metric) {
|
|
248
|
+
metric = {
|
|
249
|
+
operation: data.operation,
|
|
250
|
+
model: data.model,
|
|
251
|
+
count: 0,
|
|
252
|
+
errorCount: 0,
|
|
253
|
+
latency: {
|
|
254
|
+
sum: 0,
|
|
255
|
+
min: Infinity,
|
|
256
|
+
max: 0
|
|
257
|
+
},
|
|
258
|
+
tokens: {
|
|
259
|
+
prompt: 0,
|
|
260
|
+
completion: 0,
|
|
261
|
+
total: 0
|
|
262
|
+
},
|
|
263
|
+
cost: 0
|
|
264
|
+
};
|
|
265
|
+
this.openaiOperations.set(key, metric);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
metric.count++;
|
|
269
|
+
if (data.error) {
|
|
270
|
+
metric.errorCount++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
metric.latency.sum += data.latency;
|
|
274
|
+
metric.latency.min = Math.min(metric.latency.min, data.latency);
|
|
275
|
+
metric.latency.max = Math.max(metric.latency.max, data.latency);
|
|
276
|
+
|
|
277
|
+
metric.tokens.prompt += data.promptTokens || 0;
|
|
278
|
+
metric.tokens.completion += data.completionTokens || 0;
|
|
279
|
+
metric.tokens.total += data.totalTokens || 0;
|
|
280
|
+
|
|
281
|
+
metric.cost += data.cost || 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Update MongoDB connection pool statistics
|
|
286
|
+
*
|
|
287
|
+
* @param {Object} stats - Pool statistics
|
|
288
|
+
* @param {number} stats.active - Active connections (checked out)
|
|
289
|
+
* @param {number} stats.idle - Idle connections (available in pool)
|
|
290
|
+
* @param {number} stats.waitQueueSize - Wait queue size
|
|
291
|
+
* @param {number} stats.total - Total connections in pool
|
|
292
|
+
*/
|
|
293
|
+
updatePoolStats(stats) {
|
|
294
|
+
this.mongodbPoolStats = {
|
|
295
|
+
active: stats.active || 0,
|
|
296
|
+
idle: stats.idle || 0,
|
|
297
|
+
waitQueueSize: stats.waitQueueSize || 0,
|
|
298
|
+
total: stats.total || (stats.active || 0) + (stats.idle || 0),
|
|
299
|
+
lastUpdated: Date.now()
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Record a circuit breaker state change
|
|
305
|
+
*
|
|
306
|
+
* @param {string} service - Service/dependency name (e.g., 'mongodb')
|
|
307
|
+
* @param {string} state - New state ('open', 'halfOpen', 'close')
|
|
308
|
+
*/
|
|
309
|
+
recordCircuitState(service, state) {
|
|
310
|
+
this.circuitBreakerStates.push({
|
|
311
|
+
service,
|
|
312
|
+
state,
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if there's data to flush
|
|
319
|
+
*/
|
|
320
|
+
hasData() {
|
|
321
|
+
return this.requests.size > 0 ||
|
|
322
|
+
this.dependencies.size > 0 ||
|
|
323
|
+
this.mongodbOperations.size > 0 ||
|
|
324
|
+
this.slowQueries.length > 0 ||
|
|
325
|
+
this.circuitBreakerStates.length > 0 ||
|
|
326
|
+
this.openaiOperations.size > 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get aggregated data for flushing to backend
|
|
331
|
+
* Returns the payload in the expected format and resets counters
|
|
332
|
+
*/
|
|
333
|
+
flush() {
|
|
334
|
+
const payload = {
|
|
335
|
+
interval: `${config.flushInterval}s`,
|
|
336
|
+
service: config.service,
|
|
337
|
+
environment: config.environment,
|
|
338
|
+
requests: [],
|
|
339
|
+
dependencies: [],
|
|
340
|
+
mongodb: [],
|
|
341
|
+
mongodbPool: null,
|
|
342
|
+
slowQueries: [],
|
|
343
|
+
circuitBreaker: [],
|
|
344
|
+
openai: []
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Convert request metrics to array
|
|
348
|
+
for (const metric of this.requests.values()) {
|
|
349
|
+
payload.requests.push({
|
|
350
|
+
method: metric.method,
|
|
351
|
+
route: metric.route,
|
|
352
|
+
count: metric.count,
|
|
353
|
+
errorCount: metric.errorCount,
|
|
354
|
+
latency: {
|
|
355
|
+
sum: Math.round(metric.latency.sum),
|
|
356
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
357
|
+
max: Math.round(metric.latency.max)
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Convert dependency metrics to array
|
|
363
|
+
for (const metric of this.dependencies.values()) {
|
|
364
|
+
payload.dependencies.push({
|
|
365
|
+
name: metric.name,
|
|
366
|
+
type: metric.type,
|
|
367
|
+
count: metric.count,
|
|
368
|
+
errorCount: metric.errorCount,
|
|
369
|
+
latency: {
|
|
370
|
+
sum: Math.round(metric.latency.sum),
|
|
371
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
372
|
+
max: Math.round(metric.latency.max)
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Convert MongoDB operation metrics to array
|
|
378
|
+
for (const metric of this.mongodbOperations.values()) {
|
|
379
|
+
payload.mongodb.push({
|
|
380
|
+
collection: metric.collection,
|
|
381
|
+
operation: metric.operation,
|
|
382
|
+
count: metric.count,
|
|
383
|
+
errorCount: metric.errorCount,
|
|
384
|
+
latency: {
|
|
385
|
+
sum: Math.round(metric.latency.sum),
|
|
386
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
387
|
+
max: Math.round(metric.latency.max)
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Include MongoDB pool stats if available
|
|
393
|
+
if (this.mongodbPoolStats?.lastUpdated) {
|
|
394
|
+
payload.mongodbPool = { ...this.mongodbPoolStats };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Include slow queries
|
|
398
|
+
payload.slowQueries = [...this.slowQueries];
|
|
399
|
+
|
|
400
|
+
// Include circuit breaker state changes
|
|
401
|
+
payload.circuitBreaker = [...this.circuitBreakerStates];
|
|
402
|
+
|
|
403
|
+
// Convert OpenAI operation metrics to array
|
|
404
|
+
for (const metric of this.openaiOperations.values()) {
|
|
405
|
+
payload.openai.push({
|
|
406
|
+
operation: metric.operation,
|
|
407
|
+
model: metric.model,
|
|
408
|
+
count: metric.count,
|
|
409
|
+
errorCount: metric.errorCount,
|
|
410
|
+
latency: {
|
|
411
|
+
sum: Math.round(metric.latency.sum),
|
|
412
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
413
|
+
max: Math.round(metric.latency.max)
|
|
414
|
+
},
|
|
415
|
+
tokens: {
|
|
416
|
+
prompt: metric.tokens.prompt,
|
|
417
|
+
completion: metric.tokens.completion,
|
|
418
|
+
total: metric.tokens.total
|
|
419
|
+
},
|
|
420
|
+
cost: Math.round(metric.cost * 1000000) / 1000000 // 6 decimal places
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Reset for next interval
|
|
425
|
+
this.reset();
|
|
426
|
+
|
|
427
|
+
return payload;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get current metrics count (for monitoring/testing)
|
|
432
|
+
*/
|
|
433
|
+
getStats() {
|
|
434
|
+
return {
|
|
435
|
+
requestMetrics: this.requests.size,
|
|
436
|
+
dependencyMetrics: this.dependencies.size,
|
|
437
|
+
mongodbMetrics: this.mongodbOperations.size,
|
|
438
|
+
openaiMetrics: this.openaiOperations.size,
|
|
439
|
+
slowQueries: this.slowQueries.length,
|
|
440
|
+
circuitBreakerEvents: this.circuitBreakerStates.length,
|
|
441
|
+
uniqueRoutes: this.routeRegistry.size,
|
|
442
|
+
unhandledErrors: this.unhandledErrors
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Singleton instance
|
|
448
|
+
let instance = null;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get the singleton aggregator instance
|
|
452
|
+
*/
|
|
453
|
+
export function getAggregator() {
|
|
454
|
+
if (!instance) {
|
|
455
|
+
instance = new MetricsAggregator();
|
|
456
|
+
}
|
|
457
|
+
return instance;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export default {
|
|
461
|
+
MetricsAggregator,
|
|
462
|
+
getAggregator
|
|
463
|
+
};
|