crawlforge-mcp-server 3.0.0

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.
Files changed (75) hide show
  1. package/CLAUDE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/package.json +115 -0
  5. package/server.js +1963 -0
  6. package/setup.js +112 -0
  7. package/src/constants/config.js +615 -0
  8. package/src/core/ActionExecutor.js +1104 -0
  9. package/src/core/AlertNotificationSystem.js +601 -0
  10. package/src/core/AuthManager.js +315 -0
  11. package/src/core/ChangeTracker.js +2306 -0
  12. package/src/core/JobManager.js +687 -0
  13. package/src/core/LLMsTxtAnalyzer.js +753 -0
  14. package/src/core/LocalizationManager.js +1615 -0
  15. package/src/core/PerformanceManager.js +828 -0
  16. package/src/core/ResearchOrchestrator.js +1327 -0
  17. package/src/core/SnapshotManager.js +1037 -0
  18. package/src/core/StealthBrowserManager.js +1795 -0
  19. package/src/core/WebhookDispatcher.js +745 -0
  20. package/src/core/analysis/ContentAnalyzer.js +749 -0
  21. package/src/core/analysis/LinkAnalyzer.js +972 -0
  22. package/src/core/cache/CacheManager.js +821 -0
  23. package/src/core/connections/ConnectionPool.js +553 -0
  24. package/src/core/crawlers/BFSCrawler.js +845 -0
  25. package/src/core/integrations/PerformanceIntegration.js +377 -0
  26. package/src/core/llm/AnthropicProvider.js +135 -0
  27. package/src/core/llm/LLMManager.js +415 -0
  28. package/src/core/llm/LLMProvider.js +97 -0
  29. package/src/core/llm/OpenAIProvider.js +127 -0
  30. package/src/core/processing/BrowserProcessor.js +986 -0
  31. package/src/core/processing/ContentProcessor.js +505 -0
  32. package/src/core/processing/PDFProcessor.js +448 -0
  33. package/src/core/processing/StreamProcessor.js +673 -0
  34. package/src/core/queue/QueueManager.js +98 -0
  35. package/src/core/workers/WorkerPool.js +585 -0
  36. package/src/core/workers/worker.js +743 -0
  37. package/src/monitoring/healthCheck.js +600 -0
  38. package/src/monitoring/metrics.js +761 -0
  39. package/src/optimization/wave3-optimizations.js +932 -0
  40. package/src/security/security-patches.js +120 -0
  41. package/src/security/security-tests.js +355 -0
  42. package/src/security/wave3-security.js +652 -0
  43. package/src/tools/advanced/BatchScrapeTool.js +1089 -0
  44. package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
  45. package/src/tools/crawl/crawlDeep.js +449 -0
  46. package/src/tools/crawl/mapSite.js +400 -0
  47. package/src/tools/extract/analyzeContent.js +624 -0
  48. package/src/tools/extract/extractContent.js +329 -0
  49. package/src/tools/extract/processDocument.js +503 -0
  50. package/src/tools/extract/summarizeContent.js +376 -0
  51. package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
  52. package/src/tools/research/deepResearch.js +706 -0
  53. package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
  54. package/src/tools/search/adapters/googleSearch.js +236 -0
  55. package/src/tools/search/adapters/searchProviderFactory.js +96 -0
  56. package/src/tools/search/queryExpander.js +543 -0
  57. package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
  58. package/src/tools/search/ranking/ResultRanker.js +497 -0
  59. package/src/tools/search/searchWeb.js +482 -0
  60. package/src/tools/tracking/trackChanges.js +1355 -0
  61. package/src/utils/CircuitBreaker.js +515 -0
  62. package/src/utils/ErrorHandlingConfig.js +342 -0
  63. package/src/utils/HumanBehaviorSimulator.js +569 -0
  64. package/src/utils/Logger.js +568 -0
  65. package/src/utils/MemoryMonitor.js +173 -0
  66. package/src/utils/RetryManager.js +386 -0
  67. package/src/utils/contentUtils.js +588 -0
  68. package/src/utils/domainFilter.js +612 -0
  69. package/src/utils/inputValidation.js +766 -0
  70. package/src/utils/rateLimiter.js +196 -0
  71. package/src/utils/robotsChecker.js +91 -0
  72. package/src/utils/securityMiddleware.js +416 -0
  73. package/src/utils/sitemapParser.js +678 -0
  74. package/src/utils/ssrfProtection.js +640 -0
  75. package/src/utils/urlNormalizer.js +168 -0
@@ -0,0 +1,761 @@
1
+ /**
2
+ * Metrics Collection System for CrawlForge MCP Server
3
+ * Comprehensive performance metrics, analytics, and reporting
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { performance } from 'perf_hooks';
10
+ import { createLogger } from '../utils/Logger.js';
11
+
12
+ const logger = createLogger('Metrics');
13
+
14
+ export class MetricsCollector extends EventEmitter {
15
+ constructor(options = {}) {
16
+ super();
17
+
18
+ this.options = {
19
+ collectInterval: options.collectInterval || 10000, // 10 seconds
20
+ retentionPeriod: options.retentionPeriod || 7 * 24 * 60 * 60 * 1000, // 7 days
21
+ exportInterval: options.exportInterval || 60000, // 1 minute
22
+ exportPath: options.exportPath || './cache/metrics',
23
+ enableFileExport: options.enableFileExport !== false,
24
+ enableRealtimeMetrics: options.enableRealtimeMetrics !== false,
25
+ ...options
26
+ };
27
+
28
+ // Core metrics storage
29
+ this.metrics = {
30
+ system: {
31
+ startTime: Date.now(),
32
+ totalRequests: 0,
33
+ totalErrors: 0,
34
+ totalResponseTime: 0,
35
+ toolUsage: new Map(),
36
+ errorsByType: new Map(),
37
+ responseTimeHistogram: new Map(),
38
+ memoryUsageHistory: [],
39
+ cpuUsageHistory: []
40
+ },
41
+ tools: {
42
+ // Tool-specific metrics
43
+ fetch_url: this.createToolMetrics(),
44
+ search_web: this.createToolMetrics(),
45
+ crawl_deep: this.createToolMetrics(),
46
+ map_site: this.createToolMetrics(),
47
+ extract_content: this.createToolMetrics(),
48
+ process_document: this.createToolMetrics(),
49
+ summarize_content: this.createToolMetrics(),
50
+ analyze_content: this.createToolMetrics(),
51
+ extract_text: this.createToolMetrics(),
52
+ extract_links: this.createToolMetrics(),
53
+ extract_metadata: this.createToolMetrics(),
54
+ scrape_structured: this.createToolMetrics()
55
+ },
56
+ cache: {
57
+ hits: 0,
58
+ misses: 0,
59
+ writes: 0,
60
+ evictions: 0,
61
+ totalSize: 0,
62
+ hitRateHistory: [],
63
+ avgResponseTimeCache: 0,
64
+ avgResponseTimeNonCache: 0
65
+ },
66
+ performance: {
67
+ workerPool: {
68
+ activeWorkers: 0,
69
+ queuedTasks: 0,
70
+ completedTasks: 0,
71
+ failedTasks: 0,
72
+ avgTaskDuration: 0
73
+ },
74
+ connectionPool: {
75
+ activeConnections: 0,
76
+ totalConnections: 0,
77
+ connectionErrors: 0,
78
+ avgConnectionTime: 0
79
+ },
80
+ queue: {
81
+ pending: 0,
82
+ processing: 0,
83
+ completed: 0,
84
+ failed: 0,
85
+ avgWaitTime: 0
86
+ }
87
+ },
88
+ realtime: {
89
+ requestsPerSecond: 0,
90
+ errorsPerSecond: 0,
91
+ avgResponseTime: 0,
92
+ activeOperations: 0,
93
+ memoryUsage: 0,
94
+ cpuUsage: 0
95
+ }
96
+ };
97
+
98
+ // Time-series data for trends
99
+ this.timeSeries = {
100
+ requests: [],
101
+ errors: [],
102
+ responseTime: [],
103
+ memoryUsage: [],
104
+ cpuUsage: [],
105
+ cacheHitRate: []
106
+ };
107
+
108
+ this.isCollecting = false;
109
+ this.collectionTimer = null;
110
+ this.exportTimer = null;
111
+ }
112
+
113
+ /**
114
+ * Start metrics collection
115
+ */
116
+ async start() {
117
+ if (this.isCollecting) {
118
+ logger.warn('Metrics collection already running');
119
+ return;
120
+ }
121
+
122
+ this.isCollecting = true;
123
+ this.metrics.system.startTime = Date.now();
124
+
125
+ // Ensure export directory exists
126
+ if (this.options.enableFileExport) {
127
+ await this.ensureExportDirectory();
128
+ }
129
+
130
+ // Start collection intervals
131
+ this.collectionTimer = setInterval(() => {
132
+ this.collectSystemMetrics();
133
+ }, this.options.collectInterval);
134
+
135
+ if (this.options.enableFileExport) {
136
+ this.exportTimer = setInterval(() => {
137
+ this.exportMetrics();
138
+ }, this.options.exportInterval);
139
+ }
140
+
141
+ logger.info('Metrics collection started', {
142
+ interval: this.options.collectInterval,
143
+ exportInterval: this.options.exportInterval,
144
+ exportPath: this.options.exportPath
145
+ });
146
+
147
+ this.emit('started');
148
+ }
149
+
150
+ /**
151
+ * Stop metrics collection
152
+ */
153
+ async stop() {
154
+ if (!this.isCollecting) {
155
+ return;
156
+ }
157
+
158
+ this.isCollecting = false;
159
+
160
+ if (this.collectionTimer) {
161
+ clearInterval(this.collectionTimer);
162
+ this.collectionTimer = null;
163
+ }
164
+
165
+ if (this.exportTimer) {
166
+ clearInterval(this.exportTimer);
167
+ this.exportTimer = null;
168
+ }
169
+
170
+ // Final export
171
+ if (this.options.enableFileExport) {
172
+ await this.exportMetrics();
173
+ }
174
+
175
+ logger.info('Metrics collection stopped');
176
+ this.emit('stopped');
177
+ }
178
+
179
+ /**
180
+ * Record a tool execution
181
+ * @param {string} toolName - Name of the tool
182
+ * @param {number} responseTime - Execution time in milliseconds
183
+ * @param {boolean} isError - Whether the execution resulted in an error
184
+ * @param {Object} metadata - Additional metadata
185
+ */
186
+ recordToolExecution(toolName, responseTime, isError = false, metadata = {}) {
187
+ const timestamp = Date.now();
188
+
189
+ // Update system metrics
190
+ this.metrics.system.totalRequests++;
191
+ this.metrics.system.totalResponseTime += responseTime;
192
+
193
+ if (isError) {
194
+ this.metrics.system.totalErrors++;
195
+ const errorType = metadata.errorType || 'unknown';
196
+ this.metrics.system.errorsByType.set(
197
+ errorType,
198
+ (this.metrics.system.errorsByType.get(errorType) || 0) + 1
199
+ );
200
+ }
201
+
202
+ // Update tool-specific metrics
203
+ if (this.metrics.tools[toolName]) {
204
+ const toolMetrics = this.metrics.tools[toolName];
205
+ toolMetrics.totalCalls++;
206
+ toolMetrics.totalResponseTime += responseTime;
207
+
208
+ if (isError) {
209
+ toolMetrics.totalErrors++;
210
+ } else {
211
+ toolMetrics.successfulCalls++;
212
+ }
213
+
214
+ // Update percentiles
215
+ toolMetrics.responseTimes.push(responseTime);
216
+ if (toolMetrics.responseTimes.length > 1000) {
217
+ toolMetrics.responseTimes = toolMetrics.responseTimes.slice(-1000);
218
+ }
219
+
220
+ // Calculate running averages
221
+ toolMetrics.avgResponseTime = toolMetrics.totalResponseTime / toolMetrics.totalCalls;
222
+ toolMetrics.errorRate = toolMetrics.totalErrors / toolMetrics.totalCalls;
223
+ toolMetrics.lastUsed = timestamp;
224
+ }
225
+
226
+ // Update usage tracking
227
+ this.metrics.system.toolUsage.set(
228
+ toolName,
229
+ (this.metrics.system.toolUsage.get(toolName) || 0) + 1
230
+ );
231
+
232
+ // Update response time histogram
233
+ const bucket = this.getResponseTimeBucket(responseTime);
234
+ this.metrics.system.responseTimeHistogram.set(
235
+ bucket,
236
+ (this.metrics.system.responseTimeHistogram.get(bucket) || 0) + 1
237
+ );
238
+
239
+ // Add to time series
240
+ this.timeSeries.requests.push({ timestamp, value: 1 });
241
+ if (isError) {
242
+ this.timeSeries.errors.push({ timestamp, value: 1 });
243
+ }
244
+ this.timeSeries.responseTime.push({ timestamp, value: responseTime });
245
+
246
+ // Clean old time series data
247
+ this.cleanTimeSeries();
248
+
249
+ // Emit event for real-time monitoring
250
+ this.emit('toolExecution', {
251
+ toolName,
252
+ responseTime,
253
+ isError,
254
+ metadata,
255
+ timestamp
256
+ });
257
+
258
+ logger.debug('Recorded tool execution', {
259
+ tool: toolName,
260
+ responseTime,
261
+ isError,
262
+ timestamp
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Record cache operation
268
+ * @param {string} operation - 'hit', 'miss', 'write', 'eviction'
269
+ * @param {Object} metadata - Additional metadata
270
+ */
271
+ recordCacheOperation(operation, metadata = {}) {
272
+ const cache = this.metrics.cache;
273
+
274
+ switch (operation) {
275
+ case 'hit':
276
+ cache.hits++;
277
+ if (metadata.responseTime) {
278
+ cache.avgResponseTimeCache = this.updateAverage(
279
+ cache.avgResponseTimeCache,
280
+ metadata.responseTime,
281
+ cache.hits
282
+ );
283
+ }
284
+ break;
285
+ case 'miss':
286
+ cache.misses++;
287
+ if (metadata.responseTime) {
288
+ cache.avgResponseTimeNonCache = this.updateAverage(
289
+ cache.avgResponseTimeNonCache,
290
+ metadata.responseTime,
291
+ cache.misses
292
+ );
293
+ }
294
+ break;
295
+ case 'write':
296
+ cache.writes++;
297
+ break;
298
+ case 'eviction':
299
+ cache.evictions++;
300
+ break;
301
+ }
302
+
303
+ if (metadata.cacheSize !== undefined) {
304
+ cache.totalSize = metadata.cacheSize;
305
+ }
306
+
307
+ // Calculate and store hit rate
308
+ const total = cache.hits + cache.misses;
309
+ if (total > 0) {
310
+ const hitRate = cache.hits / total;
311
+ cache.hitRateHistory.push({
312
+ timestamp: Date.now(),
313
+ value: hitRate
314
+ });
315
+
316
+ // Keep only recent history
317
+ if (cache.hitRateHistory.length > 1000) {
318
+ cache.hitRateHistory = cache.hitRateHistory.slice(-1000);
319
+ }
320
+
321
+ this.timeSeries.cacheHitRate.push({
322
+ timestamp: Date.now(),
323
+ value: hitRate
324
+ });
325
+ }
326
+
327
+ this.emit('cacheOperation', {
328
+ operation,
329
+ metadata,
330
+ currentHitRate: total > 0 ? cache.hits / total : 0
331
+ });
332
+ }
333
+
334
+ /**
335
+ * Record worker pool metrics
336
+ * @param {Object} stats - Worker pool statistics
337
+ */
338
+ recordWorkerPoolMetrics(stats) {
339
+ Object.assign(this.metrics.performance.workerPool, stats);
340
+ this.emit('workerPoolMetrics', stats);
341
+ }
342
+
343
+ /**
344
+ * Record connection pool metrics
345
+ * @param {Object} stats - Connection pool statistics
346
+ */
347
+ recordConnectionPoolMetrics(stats) {
348
+ Object.assign(this.metrics.performance.connectionPool, stats);
349
+ this.emit('connectionPoolMetrics', stats);
350
+ }
351
+
352
+ /**
353
+ * Record queue metrics
354
+ * @param {Object} stats - Queue statistics
355
+ */
356
+ recordQueueMetrics(stats) {
357
+ Object.assign(this.metrics.performance.queue, stats);
358
+ this.emit('queueMetrics', stats);
359
+ }
360
+
361
+ /**
362
+ * Collect system-level metrics
363
+ */
364
+ collectSystemMetrics() {
365
+ const timestamp = Date.now();
366
+
367
+ // Memory usage
368
+ const memUsage = process.memoryUsage();
369
+ this.metrics.system.memoryUsageHistory.push({
370
+ timestamp,
371
+ heapUsed: memUsage.heapUsed,
372
+ heapTotal: memUsage.heapTotal,
373
+ rss: memUsage.rss,
374
+ external: memUsage.external
375
+ });
376
+
377
+ this.timeSeries.memoryUsage.push({
378
+ timestamp,
379
+ value: memUsage.heapUsed
380
+ });
381
+
382
+ // CPU usage (simplified)
383
+ const cpuUsage = process.cpuUsage();
384
+ this.metrics.system.cpuUsageHistory.push({
385
+ timestamp,
386
+ user: cpuUsage.user,
387
+ system: cpuUsage.system
388
+ });
389
+
390
+ // Update realtime metrics
391
+ this.updateRealtimeMetrics();
392
+
393
+ // Clean old history
394
+ this.cleanHistoryData();
395
+ this.cleanTimeSeries();
396
+
397
+ this.emit('systemMetrics', {
398
+ memory: memUsage,
399
+ cpu: cpuUsage,
400
+ timestamp
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Update realtime metrics
406
+ */
407
+ updateRealtimeMetrics() {
408
+ const now = Date.now();
409
+ const oneMinuteAgo = now - 60000;
410
+
411
+ // Calculate requests per second
412
+ const recentRequests = this.timeSeries.requests.filter(r => r.timestamp > oneMinuteAgo);
413
+ this.metrics.realtime.requestsPerSecond = recentRequests.length / 60;
414
+
415
+ // Calculate errors per second
416
+ const recentErrors = this.timeSeries.errors.filter(e => e.timestamp > oneMinuteAgo);
417
+ this.metrics.realtime.errorsPerSecond = recentErrors.length / 60;
418
+
419
+ // Calculate average response time
420
+ const recentResponseTimes = this.timeSeries.responseTime.filter(r => r.timestamp > oneMinuteAgo);
421
+ if (recentResponseTimes.length > 0) {
422
+ this.metrics.realtime.avgResponseTime =
423
+ recentResponseTimes.reduce((sum, r) => sum + r.value, 0) / recentResponseTimes.length;
424
+ }
425
+
426
+ // Memory and CPU from latest readings
427
+ if (this.metrics.system.memoryUsageHistory.length > 0) {
428
+ const latest = this.metrics.system.memoryUsageHistory[this.metrics.system.memoryUsageHistory.length - 1];
429
+ this.metrics.realtime.memoryUsage = latest.heapUsed;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Get comprehensive metrics report
435
+ * @param {Object} options - Report options
436
+ * @returns {Object} Metrics report
437
+ */
438
+ getMetrics(options = {}) {
439
+ const {
440
+ includeTimeSeries = false,
441
+ includeHistory = false,
442
+ includePercentiles = true,
443
+ timeRange = null
444
+ } = options;
445
+
446
+ const report = {
447
+ timestamp: Date.now(),
448
+ uptime: Date.now() - this.metrics.system.startTime,
449
+ summary: this.generateSummary(),
450
+ tools: this.generateToolMetrics(includePercentiles),
451
+ cache: { ...this.metrics.cache },
452
+ performance: { ...this.metrics.performance },
453
+ realtime: { ...this.metrics.realtime }
454
+ };
455
+
456
+ if (includeTimeSeries) {
457
+ report.timeSeries = this.filterTimeSeriesByRange(timeRange);
458
+ }
459
+
460
+ if (includeHistory) {
461
+ report.history = {
462
+ memory: this.filterHistoryByRange(this.metrics.system.memoryUsageHistory, timeRange),
463
+ cpu: this.filterHistoryByRange(this.metrics.system.cpuUsageHistory, timeRange)
464
+ };
465
+ }
466
+
467
+ return report;
468
+ }
469
+
470
+ /**
471
+ * Generate summary metrics
472
+ */
473
+ generateSummary() {
474
+ const { system } = this.metrics;
475
+ const uptime = Date.now() - system.startTime;
476
+
477
+ return {
478
+ totalRequests: system.totalRequests,
479
+ totalErrors: system.totalErrors,
480
+ errorRate: system.totalRequests > 0 ? system.totalErrors / system.totalRequests : 0,
481
+ avgResponseTime: system.totalRequests > 0 ? system.totalResponseTime / system.totalRequests : 0,
482
+ requestsPerSecond: system.totalRequests / (uptime / 1000),
483
+ uptime,
484
+ mostUsedTool: this.getMostUsedTool(),
485
+ cacheHitRate: this.getCacheHitRate()
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Generate tool-specific metrics
491
+ */
492
+ generateToolMetrics(includePercentiles = true) {
493
+ const toolMetrics = {};
494
+
495
+ for (const [toolName, metrics] of Object.entries(this.metrics.tools)) {
496
+ toolMetrics[toolName] = {
497
+ totalCalls: metrics.totalCalls,
498
+ successfulCalls: metrics.successfulCalls,
499
+ totalErrors: metrics.totalErrors,
500
+ avgResponseTime: metrics.avgResponseTime,
501
+ errorRate: metrics.errorRate,
502
+ lastUsed: metrics.lastUsed
503
+ };
504
+
505
+ if (includePercentiles && metrics.responseTimes.length > 0) {
506
+ toolMetrics[toolName].percentiles = this.calculatePercentiles(metrics.responseTimes);
507
+ }
508
+ }
509
+
510
+ return toolMetrics;
511
+ }
512
+
513
+ /**
514
+ * Calculate percentiles for response times
515
+ */
516
+ calculatePercentiles(values) {
517
+ const sorted = [...values].sort((a, b) => a - b);
518
+ const len = sorted.length;
519
+
520
+ return {
521
+ p50: sorted[Math.floor(len * 0.5)],
522
+ p75: sorted[Math.floor(len * 0.75)],
523
+ p90: sorted[Math.floor(len * 0.90)],
524
+ p95: sorted[Math.floor(len * 0.95)],
525
+ p99: sorted[Math.floor(len * 0.99)]
526
+ };
527
+ }
528
+
529
+ /**
530
+ * Export metrics to file
531
+ */
532
+ async exportMetrics() {
533
+ if (!this.options.enableFileExport) {
534
+ return;
535
+ }
536
+
537
+ try {
538
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
539
+ const metrics = this.getMetrics({ includeTimeSeries: true, includeHistory: true });
540
+
541
+ // Export comprehensive metrics
542
+ const metricsPath = path.join(this.options.exportPath, `metrics-${timestamp}.json`);
543
+ await fs.writeFile(metricsPath, JSON.stringify(metrics, null, 2));
544
+
545
+ // Export CSV summary for analysis tools
546
+ const csvPath = path.join(this.options.exportPath, `metrics-summary-${timestamp}.csv`);
547
+ await this.exportCSV(metrics, csvPath);
548
+
549
+ logger.debug('Metrics exported', { metricsPath, csvPath });
550
+ this.emit('metricsExported', { metricsPath, csvPath });
551
+
552
+ } catch (error) {
553
+ logger.error('Failed to export metrics', { error: error.message });
554
+ this.emit('exportError', error);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Export metrics as CSV
560
+ */
561
+ async exportCSV(metrics, filePath) {
562
+ const csvLines = [];
563
+
564
+ // Header
565
+ csvLines.push('timestamp,tool,totalCalls,successfulCalls,errorRate,avgResponseTime,p95ResponseTime');
566
+
567
+ // Tool data
568
+ for (const [toolName, toolMetrics] of Object.entries(metrics.tools)) {
569
+ csvLines.push([
570
+ metrics.timestamp,
571
+ toolName,
572
+ toolMetrics.totalCalls,
573
+ toolMetrics.successfulCalls,
574
+ (toolMetrics.errorRate * 100).toFixed(2),
575
+ toolMetrics.avgResponseTime.toFixed(2),
576
+ toolMetrics.percentiles?.p95 || 0
577
+ ].join(','));
578
+ }
579
+
580
+ await fs.writeFile(filePath, csvLines.join('\n'));
581
+ }
582
+
583
+ /**
584
+ * Create tool metrics structure
585
+ */
586
+ createToolMetrics() {
587
+ return {
588
+ totalCalls: 0,
589
+ successfulCalls: 0,
590
+ totalErrors: 0,
591
+ totalResponseTime: 0,
592
+ avgResponseTime: 0,
593
+ errorRate: 0,
594
+ responseTimes: [],
595
+ lastUsed: null
596
+ };
597
+ }
598
+
599
+ /**
600
+ * Get response time bucket for histogram
601
+ */
602
+ getResponseTimeBucket(responseTime) {
603
+ if (responseTime < 100) return '0-100ms';
604
+ if (responseTime < 500) return '100-500ms';
605
+ if (responseTime < 1000) return '500ms-1s';
606
+ if (responseTime < 2000) return '1-2s';
607
+ if (responseTime < 5000) return '2-5s';
608
+ return '5s+';
609
+ }
610
+
611
+ /**
612
+ * Update running average
613
+ */
614
+ updateAverage(currentAvg, newValue, count) {
615
+ return ((currentAvg * (count - 1)) + newValue) / count;
616
+ }
617
+
618
+ /**
619
+ * Get most used tool
620
+ */
621
+ getMostUsedTool() {
622
+ let maxUsage = 0;
623
+ let mostUsed = null;
624
+
625
+ for (const [tool, usage] of this.metrics.system.toolUsage.entries()) {
626
+ if (usage > maxUsage) {
627
+ maxUsage = usage;
628
+ mostUsed = tool;
629
+ }
630
+ }
631
+
632
+ return mostUsed;
633
+ }
634
+
635
+ /**
636
+ * Get current cache hit rate
637
+ */
638
+ getCacheHitRate() {
639
+ const { cache } = this.metrics;
640
+ const total = cache.hits + cache.misses;
641
+ return total > 0 ? cache.hits / total : 0;
642
+ }
643
+
644
+ /**
645
+ * Clean old time series data
646
+ */
647
+ cleanTimeSeries() {
648
+ const cutoff = Date.now() - this.options.retentionPeriod;
649
+
650
+ for (const series of Object.values(this.timeSeries)) {
651
+ const originalLength = series.length;
652
+ const filtered = series.filter(item => item.timestamp > cutoff);
653
+ if (filtered.length !== originalLength) {
654
+ series.length = 0;
655
+ series.push(...filtered);
656
+ }
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Clean old history data
662
+ */
663
+ cleanHistoryData() {
664
+ const cutoff = Date.now() - this.options.retentionPeriod;
665
+
666
+ this.metrics.system.memoryUsageHistory = this.metrics.system.memoryUsageHistory
667
+ .filter(item => item.timestamp > cutoff);
668
+
669
+ this.metrics.system.cpuUsageHistory = this.metrics.system.cpuUsageHistory
670
+ .filter(item => item.timestamp > cutoff);
671
+ }
672
+
673
+ /**
674
+ * Filter time series by time range
675
+ */
676
+ filterTimeSeriesByRange(timeRange) {
677
+ if (!timeRange) {
678
+ return this.timeSeries;
679
+ }
680
+
681
+ const { start, end } = timeRange;
682
+ const filtered = {};
683
+
684
+ for (const [key, series] of Object.entries(this.timeSeries)) {
685
+ filtered[key] = series.filter(item =>
686
+ item.timestamp >= start && item.timestamp <= end
687
+ );
688
+ }
689
+
690
+ return filtered;
691
+ }
692
+
693
+ /**
694
+ * Filter history by time range
695
+ */
696
+ filterHistoryByRange(history, timeRange) {
697
+ if (!timeRange || !history) {
698
+ return history;
699
+ }
700
+
701
+ const { start, end } = timeRange;
702
+ return history.filter(item =>
703
+ item.timestamp >= start && item.timestamp <= end
704
+ );
705
+ }
706
+
707
+ /**
708
+ * Ensure export directory exists
709
+ */
710
+ async ensureExportDirectory() {
711
+ try {
712
+ await fs.access(this.options.exportPath);
713
+ } catch (error) {
714
+ await fs.mkdir(this.options.exportPath, { recursive: true });
715
+ }
716
+ }
717
+
718
+ /**
719
+ * Reset all metrics
720
+ */
721
+ reset() {
722
+ this.metrics.system = {
723
+ startTime: Date.now(),
724
+ totalRequests: 0,
725
+ totalErrors: 0,
726
+ totalResponseTime: 0,
727
+ toolUsage: new Map(),
728
+ errorsByType: new Map(),
729
+ responseTimeHistogram: new Map(),
730
+ memoryUsageHistory: [],
731
+ cpuUsageHistory: []
732
+ };
733
+
734
+ // Reset tool metrics
735
+ for (const toolName of Object.keys(this.metrics.tools)) {
736
+ this.metrics.tools[toolName] = this.createToolMetrics();
737
+ }
738
+
739
+ // Reset cache metrics
740
+ this.metrics.cache = {
741
+ hits: 0,
742
+ misses: 0,
743
+ writes: 0,
744
+ evictions: 0,
745
+ totalSize: 0,
746
+ hitRateHistory: [],
747
+ avgResponseTimeCache: 0,
748
+ avgResponseTimeNonCache: 0
749
+ };
750
+
751
+ // Clear time series
752
+ for (const series of Object.values(this.timeSeries)) {
753
+ series.length = 0;
754
+ }
755
+
756
+ logger.info('Metrics reset');
757
+ this.emit('reset');
758
+ }
759
+ }
760
+
761
+ export default MetricsCollector;