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
JavaScript
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();