@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/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 && 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
- };
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
+ };