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.
- package/CLAUDE.md +315 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/package.json +115 -0
- package/server.js +1963 -0
- package/setup.js +112 -0
- package/src/constants/config.js +615 -0
- package/src/core/ActionExecutor.js +1104 -0
- package/src/core/AlertNotificationSystem.js +601 -0
- package/src/core/AuthManager.js +315 -0
- package/src/core/ChangeTracker.js +2306 -0
- package/src/core/JobManager.js +687 -0
- package/src/core/LLMsTxtAnalyzer.js +753 -0
- package/src/core/LocalizationManager.js +1615 -0
- package/src/core/PerformanceManager.js +828 -0
- package/src/core/ResearchOrchestrator.js +1327 -0
- package/src/core/SnapshotManager.js +1037 -0
- package/src/core/StealthBrowserManager.js +1795 -0
- package/src/core/WebhookDispatcher.js +745 -0
- package/src/core/analysis/ContentAnalyzer.js +749 -0
- package/src/core/analysis/LinkAnalyzer.js +972 -0
- package/src/core/cache/CacheManager.js +821 -0
- package/src/core/connections/ConnectionPool.js +553 -0
- package/src/core/crawlers/BFSCrawler.js +845 -0
- package/src/core/integrations/PerformanceIntegration.js +377 -0
- package/src/core/llm/AnthropicProvider.js +135 -0
- package/src/core/llm/LLMManager.js +415 -0
- package/src/core/llm/LLMProvider.js +97 -0
- package/src/core/llm/OpenAIProvider.js +127 -0
- package/src/core/processing/BrowserProcessor.js +986 -0
- package/src/core/processing/ContentProcessor.js +505 -0
- package/src/core/processing/PDFProcessor.js +448 -0
- package/src/core/processing/StreamProcessor.js +673 -0
- package/src/core/queue/QueueManager.js +98 -0
- package/src/core/workers/WorkerPool.js +585 -0
- package/src/core/workers/worker.js +743 -0
- package/src/monitoring/healthCheck.js +600 -0
- package/src/monitoring/metrics.js +761 -0
- package/src/optimization/wave3-optimizations.js +932 -0
- package/src/security/security-patches.js +120 -0
- package/src/security/security-tests.js +355 -0
- package/src/security/wave3-security.js +652 -0
- package/src/tools/advanced/BatchScrapeTool.js +1089 -0
- package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
- package/src/tools/crawl/crawlDeep.js +449 -0
- package/src/tools/crawl/mapSite.js +400 -0
- package/src/tools/extract/analyzeContent.js +624 -0
- package/src/tools/extract/extractContent.js +329 -0
- package/src/tools/extract/processDocument.js +503 -0
- package/src/tools/extract/summarizeContent.js +376 -0
- package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
- package/src/tools/research/deepResearch.js +706 -0
- package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
- package/src/tools/search/adapters/googleSearch.js +236 -0
- package/src/tools/search/adapters/searchProviderFactory.js +96 -0
- package/src/tools/search/queryExpander.js +543 -0
- package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
- package/src/tools/search/ranking/ResultRanker.js +497 -0
- package/src/tools/search/searchWeb.js +482 -0
- package/src/tools/tracking/trackChanges.js +1355 -0
- package/src/utils/CircuitBreaker.js +515 -0
- package/src/utils/ErrorHandlingConfig.js +342 -0
- package/src/utils/HumanBehaviorSimulator.js +569 -0
- package/src/utils/Logger.js +568 -0
- package/src/utils/MemoryMonitor.js +173 -0
- package/src/utils/RetryManager.js +386 -0
- package/src/utils/contentUtils.js +588 -0
- package/src/utils/domainFilter.js +612 -0
- package/src/utils/inputValidation.js +766 -0
- package/src/utils/rateLimiter.js +196 -0
- package/src/utils/robotsChecker.js +91 -0
- package/src/utils/securityMiddleware.js +416 -0
- package/src/utils/sitemapParser.js +678 -0
- package/src/utils/ssrfProtection.js +640 -0
- 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;
|