UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

396 lines (395 loc) 15.3 kB
import { logger } from './api.js'; export class PerformanceMonitor { constructor() { this.metrics = new Map(); this.timers = new Map(); this.requestCounts = new Map(); this.startTime = Date.now(); // Performance thresholds this.thresholds = { responseTime: { good: 1000, // < 1s warning: 5000, // < 5s critical: 15000 // < 15s }, errorRate: { good: 0.01, // < 1% warning: 0.05, // < 5% critical: 0.15 // < 15% }, memoryUsage: { good: 0.7, // < 70% warning: 0.85, // < 85% critical: 0.95 // < 95% } }; // Start collecting system metrics every minute setInterval(() => this.collectSystemMetrics(), 60000); // Clean old metrics every 5 minutes setInterval(() => this.cleanOldMetrics(), 5 * 60000); } static getInstance() { if (!this.instance) { this.instance = new PerformanceMonitor(); } return this.instance; } /** * Record a metric */ recordMetric(name, value, type = 'gauge', tags = {}) { const metric = { name, value, timestamp: Date.now(), tags, type }; if (!this.metrics.has(name)) { this.metrics.set(name, []); } this.metrics.get(name).push(metric); // Keep only last 1000 entries per metric const entries = this.metrics.get(name); if (entries.length > 1000) { entries.splice(0, entries.length - 1000); } } /** * Start a timer */ startTimer(name) { this.timers.set(name, Date.now()); } /** * Stop a timer and record the duration */ stopTimer(name, success = true, error) { const startTime = this.timers.get(name); if (!startTime) { throw new Error(`Timer ${name} was not started`); } const duration = Date.now() - startTime; this.timers.delete(name); // Record timer metric this.recordMetric(name, duration, 'timer', { success: success.toString(), error: error || '' }); // Log slow operations if (duration > this.thresholds.responseTime.warning) { logger.log(`⚠️ Slow operation detected: ${name} took ${duration}ms`); } return { name, duration, success, error }; } /** * Record request count */ recordRequest(endpoint, method = 'unknown') { const key = `${method}:${endpoint}`; this.requestCounts.set(key, (this.requestCounts.get(key) || 0) + 1); this.recordMetric('requests', 1, 'counter', { endpoint, method }); } /** * Record error */ recordError(operation, error, severity = 'medium') { this.recordMetric('errors', 1, 'counter', { operation, error: error.substring(0, 100), // Truncate long errors severity }); if (severity === 'high') { logger.error(`🚨 High severity error in ${operation}: ${error}`); } } /** * Record cache hit/miss */ recordCacheHit(cacheType, hit) { this.recordMetric('cache_operations', 1, 'counter', { type: cacheType, result: hit ? 'hit' : 'miss' }); } /** * Record API call metrics */ recordApiCall(endpoint, duration, success, statusCode) { this.recordMetric('api_calls', 1, 'counter', { endpoint, success: success.toString(), status_code: statusCode?.toString() || '' }); this.recordMetric('api_call_duration', duration, 'timer', { endpoint, success: success.toString() }); } /** * Get current performance statistics */ getStats() { const now = Date.now(); const oneMinuteAgo = now - 60000; const oneHourAgo = now - 3600000; // Calculate requests per minute const recentRequests = this.getMetricsSince('requests', oneMinuteAgo); const requestsPerMinute = recentRequests.length; // Calculate average response time const recentTimers = this.getMetricsSince('api_call_duration', oneHourAgo); const avgResponseTime = recentTimers.length > 0 ? recentTimers.reduce((sum, m) => sum + m.value, 0) / recentTimers.length : 0; // Calculate error rate const recentRequests1h = this.getMetricsSince('requests', oneHourAgo); const recentErrors = this.getMetricsSince('errors', oneHourAgo); const errorRate = recentRequests1h.length > 0 ? recentErrors.length / recentRequests1h.length : 0; // Calculate cache hit rate const recentCacheOps = this.getMetricsSince('cache_operations', oneHourAgo); const cacheHits = recentCacheOps.filter(m => m.tags.result === 'hit').length; const cacheHitRate = recentCacheOps.length > 0 ? cacheHits / recentCacheOps.length : 0; // Get memory usage const memoryUsage = this.getMemoryUsage(); // API call statistics const recentApiCalls = this.getMetricsSince('api_calls', oneHourAgo); const successfulApiCalls = recentApiCalls.filter(m => m.tags.success === 'true').length; const avgApiDuration = recentTimers.length > 0 ? recentTimers.reduce((sum, m) => sum + m.value, 0) / recentTimers.length : 0; // Calculate health score (0-100) const healthScore = this.calculateHealthScore(avgResponseTime, errorRate, memoryUsage.percentage, cacheHitRate); return { requestsPerMinute, averageResponseTime: avgResponseTime, errorRate: errorRate * 100, // Convert to percentage cacheHitRate: cacheHitRate * 100, // Convert to percentage memoryUsage, apiCallStats: { total: recentApiCalls.length, successful: successfulApiCalls, failed: recentApiCalls.length - successfulApiCalls, avgDuration: avgApiDuration }, healthScore }; } /** * Get detailed metrics for a specific operation */ getOperationMetrics(operation) { const oneHourAgo = Date.now() - 3600000; const timerMetrics = this.getMetricsSince(operation, oneHourAgo); const errorMetrics = this.getMetricsSince('errors', oneHourAgo) .filter(m => m.tags.operation === operation); const totalCalls = timerMetrics.length; const successfulCalls = timerMetrics.filter(m => m.tags.success === 'true').length; const successRate = totalCalls > 0 ? successfulCalls / totalCalls : 0; const averageDuration = totalCalls > 0 ? timerMetrics.reduce((sum, m) => sum + m.value, 0) / totalCalls : 0; // Error breakdown const errorBreakdown = {}; errorMetrics.forEach(metric => { const errorType = metric.tags.error || 'unknown'; errorBreakdown[errorType] = (errorBreakdown[errorType] || 0) + 1; }); // Recent performance (last 50 calls) const recentPerformance = timerMetrics .slice(-50) .map(m => ({ timestamp: m.timestamp, duration: m.value, success: m.tags.success === 'true' })); return { totalCalls, successRate: successRate * 100, averageDuration, errorBreakdown, recentPerformance }; } /** * Get health status */ getHealthStatus() { const stats = this.getStats(); const issues = []; const recommendations = []; let status = 'healthy'; // Check response time if (stats.averageResponseTime > this.thresholds.responseTime.critical) { status = 'critical'; issues.push(`Very slow response time: ${stats.averageResponseTime.toFixed(0)}ms`); recommendations.push('Consider optimizing queries and using more aggressive caching'); } else if (stats.averageResponseTime > this.thresholds.responseTime.warning) { status = 'warning'; issues.push(`Slow response time: ${stats.averageResponseTime.toFixed(0)}ms`); recommendations.push('Monitor query performance and consider optimization'); } // Check error rate if (stats.errorRate > this.thresholds.errorRate.critical * 100) { status = 'critical'; issues.push(`High error rate: ${stats.errorRate.toFixed(1)}%`); recommendations.push('Investigate errors and implement additional error handling'); } else if (stats.errorRate > this.thresholds.errorRate.warning * 100 && status !== 'critical') { status = 'warning'; issues.push(`Elevated error rate: ${stats.errorRate.toFixed(1)}%`); recommendations.push('Monitor error patterns and improve error handling'); } // Check memory usage if (stats.memoryUsage.percentage > this.thresholds.memoryUsage.critical) { status = 'critical'; issues.push(`Critical memory usage: ${(stats.memoryUsage.percentage * 100).toFixed(1)}%`); recommendations.push('Implement memory optimization and consider restarting the server'); } else if (stats.memoryUsage.percentage > this.thresholds.memoryUsage.warning && status !== 'critical') { status = 'warning'; issues.push(`High memory usage: ${(stats.memoryUsage.percentage * 100).toFixed(1)}%`); recommendations.push('Monitor memory usage and clear caches if needed'); } // Check cache hit rate if (stats.cacheHitRate < 50 && status === 'healthy') { status = 'warning'; issues.push(`Low cache hit rate: ${stats.cacheHitRate.toFixed(1)}%`); recommendations.push('Review caching strategy and TTL settings'); } const uptime = Date.now() - this.startTime; return { status, issues, recommendations, uptime }; } /** * Generate performance report */ generateReport() { const stats = this.getStats(); const health = this.getHealthStatus(); const uptimeHours = (health.uptime / (1000 * 60 * 60)).toFixed(1); let report = `📊 Performance Report\n\n`; report += `🔋 Health Status: ${health.status.toUpperCase()}\n`; report += `⏱️ Uptime: ${uptimeHours} hours\n`; report += `🎯 Health Score: ${stats.healthScore}/100\n\n`; report += `📈 Key Metrics:\n`; report += `• Requests/min: ${stats.requestsPerMinute}\n`; report += `• Avg Response Time: ${stats.averageResponseTime.toFixed(0)}ms\n`; report += `• Error Rate: ${stats.errorRate.toFixed(2)}%\n`; report += `• Cache Hit Rate: ${stats.cacheHitRate.toFixed(1)}%\n`; report += `• Memory Usage: ${(stats.memoryUsage.percentage * 100).toFixed(1)}%\n\n`; report += `🌐 API Calls (last hour):\n`; report += `• Total: ${stats.apiCallStats.total}\n`; report += `• Successful: ${stats.apiCallStats.successful}\n`; report += `• Failed: ${stats.apiCallStats.failed}\n`; report += `• Avg Duration: ${stats.apiCallStats.avgDuration.toFixed(0)}ms\n\n`; if (health.issues.length > 0) { report += `⚠️ Issues:\n${health.issues.map(issue => `• ${issue}`).join('\n')}\n\n`; } if (health.recommendations.length > 0) { report += `💡 Recommendations:\n${health.recommendations.map(rec => `• ${rec}`).join('\n')}\n`; } return report; } /** * Get metrics since a specific timestamp */ getMetricsSince(name, since) { const metrics = this.metrics.get(name) || []; return metrics.filter(m => m.timestamp >= since); } /** * Get current memory usage */ getMemoryUsage() { const memUsage = process.memoryUsage(); const used = memUsage.heapUsed; const total = memUsage.heapTotal; const percentage = total > 0 ? used / total : 0; return { used, total, percentage }; } /** * Calculate overall health score */ calculateHealthScore(responseTime, errorRate, memoryUsage, cacheHitRate) { let score = 100; // Response time impact (0-30 points) if (responseTime > this.thresholds.responseTime.critical) { score -= 30; } else if (responseTime > this.thresholds.responseTime.warning) { score -= 15; } else if (responseTime > this.thresholds.responseTime.good) { score -= 5; } // Error rate impact (0-25 points) if (errorRate > this.thresholds.errorRate.critical) { score -= 25; } else if (errorRate > this.thresholds.errorRate.warning) { score -= 15; } else if (errorRate > this.thresholds.errorRate.good) { score -= 5; } // Memory usage impact (0-25 points) if (memoryUsage > this.thresholds.memoryUsage.critical) { score -= 25; } else if (memoryUsage > this.thresholds.memoryUsage.warning) { score -= 15; } else if (memoryUsage > this.thresholds.memoryUsage.good) { score -= 5; } // Cache hit rate bonus/penalty (0-20 points) if (cacheHitRate < 0.3) { score -= 20; } else if (cacheHitRate < 0.5) { score -= 10; } else if (cacheHitRate > 0.8) { score += 5; // Bonus for good cache performance } return Math.max(0, Math.min(100, score)); } /** * Collect system metrics */ collectSystemMetrics() { const memUsage = this.getMemoryUsage(); this.recordMetric('memory_usage', memUsage.percentage, 'gauge'); this.recordMetric('memory_used', memUsage.used, 'gauge'); this.recordMetric('memory_total', memUsage.total, 'gauge'); // Record uptime const uptime = Date.now() - this.startTime; this.recordMetric('uptime', uptime, 'gauge'); } /** * Clean old metrics to prevent memory leaks */ cleanOldMetrics() { const oneHourAgo = Date.now() - 3600000; let cleaned = 0; for (const [name, metrics] of this.metrics) { const filteredMetrics = metrics.filter(m => m.timestamp >= oneHourAgo); this.metrics.set(name, filteredMetrics); cleaned += metrics.length - filteredMetrics.length; } if (cleaned > 0) { logger.log(`🧹 Cleaned ${cleaned} old performance metrics`); } } } // Export singleton instance export const performanceMonitor = PerformanceMonitor.getInstance();