/** * Performance Monitoring MCP Tools for WordPress Server * Provides comprehensive performance insights and management */ import type { ToolDefinition } from "@/server/ToolRegistry.js"; import { hasCacheActivity, PerformanceMonitor, type PerformanceMetrics } from "@/performance/PerformanceMonitor.js"; import { MetricsCollector } from "@/performance/MetricsCollector.js"; import { PerformanceAnalytics, type BenchmarkComparison, type PerformanceAnomaly, } from "@/performance/PerformanceAnalytics.js"; import { toolWrapper } from "@/utils/toolWrapper.js"; import { ConfigHelpers } from "@/config/Config.js"; import { LoggerFactory } from "@/utils/logger.js"; import type { WordPressClient } from "@/client/api.js"; // Import helper functions import { calculateHealthStatus, calculatePerformanceScore, calculateCacheEfficiency, formatUptime, parseTimeframe, describeHistoryWindow, HISTORY_RETENTION_MS, processHistoricalDataForChart, downsample, formatHitRate, MAX_HISTORY_POINTS, calculateAverage, formatBenchmarkStatus, getBenchmarkImprovementDescription, calculateOverallRanking, formatAlertMessage, formatAnomalyDescription, calculateAlertStatus, calculateActiveAlertStatus, worseAlertStatus, formatPriority, formatEffort, calculateEstimatedImpact, convertToCSV, createSummaryReport, type PerformanceAlert, } from "./PerformanceHelpers.js"; // The session-wide cache figures sum every site's cache counters; wp_cache_stats and `siteSpecific` show one site. const CACHE_SCOPE_ALL_SITES = "all sites combined, since the server started"; // Benchmarks, recommendations and exports read the same session-wide metrics. const CURRENT_METRICS_SCOPE = "current metrics, all sites combined (counters since the server started)"; // PerformanceAnalytics keeps its own 24-hour history for trends, anomalies and predictions. const ANALYTICS_SCOPE = "all sites combined, from the analytics' own 24-hour history whatever the timeframe"; /** * Performance Tools Class * Provides MCP tools for WordPress performance monitoring */ export default class PerformanceTools { private monitor: PerformanceMonitor; private collector: MetricsCollector; private analytics: PerformanceAnalytics; private logger: ReturnType; private historicalDataInterval?: NodeJS.Timeout | undefined; constructor(clients?: Map) { // Initialize logger first this.logger = LoggerFactory.performance(); // Initialize performance monitoring system this.monitor = new PerformanceMonitor({ enableRealTimeMonitoring: true, enableHistoricalData: true, retentionPeriod: HISTORY_RETENTION_MS, enableAlerts: true, }); this.collector = new MetricsCollector(this.monitor, { enableRealTime: true, enableToolTracking: true, enableCacheIntegration: true, }); this.analytics = new PerformanceAnalytics(this.collector, { enablePredictiveAnalysis: true, enableAnomalyDetection: true, enableTrendAnalysis: true, }); // Register clients if provided if (clients) { for (const [siteId, client] of clients) { this.collector.registerClient(siteId, client); // Register cache manager if client has one const possibleCacheMgr = (client as Record)?.cacheManager as unknown; if (possibleCacheMgr) { this.collector.registerCacheManager(siteId, possibleCacheMgr); } } } // Only start historical data collection in production environments if (ConfigHelpers.isProd() || ConfigHelpers.isDev()) { this.startHistoricalDataCollection(); } } /** * The collector that tool dispatch reports invocations to (see ToolRegistry.setMetricsCollector). */ getMetricsCollector(): MetricsCollector { return this.collector; } /** * Get all performance monitoring tools */ getTools(): ToolDefinition[] { return [ { name: "wp_performance_stats", description: "Get real-time performance statistics and metrics. " + "Note: Top-level metrics (totalRequests, averageResponseTime, errorRate) are session-wide aggregates across all sites. " + "Per-site cache and client stats are shown in the siteSpecific section when a site parameter is provided. " + "category=tools also lists the most recent tool calls (tool, site, status, duration, error code), which is the way to see " + "what failed and why — an installed extension's own log output is not shown by the host.", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "category", type: "string", description: "Category of metrics to return (overview, requests, cache, system, tools, all)", required: false, }, { name: "format", type: "string", description: "Detail level of the response (summary, detailed, raw)", required: false, }, ], handler: this.getPerformanceStats.bind(this), }, { name: "wp_performance_history", description: "Get historical performance data and trends", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "timeframe", type: "string", description: "Time period for historical data (1h, 6h, 12h, 24h, 7d)", required: false, }, { name: "metrics", type: "array", items: { type: "string" }, description: "Specific metrics to include (responseTime, cacheHitRate, errorRate, memoryUsage, requestVolume)", required: false, }, { name: "includeTrends", type: "boolean", description: "Include trend analysis (default: true)", required: false, }, ], handler: this.getPerformanceHistory.bind(this), }, { name: "wp_performance_benchmark", description: "Compare current performance against industry benchmarks. " + "Note: Benchmarks are based on session-wide aggregated metrics across all sites, not per-site metrics.", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "category", type: "string", description: "Benchmark category (response_time, cache_performance, error_rate, system_resources, all)", required: false, }, { name: "includeRecommendations", type: "boolean", description: "Include improvement recommendations (default: true)", required: false, }, ], handler: this.getBenchmarkComparison.bind(this), }, { name: "wp_performance_alerts", description: "Get performance alerts and anomaly detection results. summary.overallStatus covers every alert recorded this " + "session (alerts never expire) and is never better than summary.currentStatus; summary.currentStatus and " + "summary.activeAlerts show only what is breaching right now, using the same rules as wp_performance_stats " + "(overview.alertStatus).", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "severity", type: "string", description: "Filter alerts by severity level (info, warning, error, critical)", required: false, }, { name: "category", type: "string", description: "Filter alerts by category (performance, cache, system, wordpress)", required: false, }, { name: "limit", type: "number", description: "Maximum number of alerts to return (default: 20)", required: false, }, { name: "includeAnomalies", type: "boolean", description: "Include detected anomalies (default: true)", required: false, }, ], handler: this.getPerformanceAlerts.bind(this), }, { name: "wp_performance_optimize", description: "Get optimization recommendations and insights", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "focus", type: "string", description: "Optimization focus area (speed, reliability, efficiency, scaling)", required: false, }, { name: "priority", type: "string", description: "Implementation timeline (quick_wins, medium_term, long_term, all)", required: false, }, { name: "includeROI", type: "boolean", description: "Include ROI estimates (default: true)", required: false, }, { name: "includePredictions", type: "boolean", description: "Include performance predictions (default: true)", required: false, }, ], handler: this.getOptimizationRecommendations.bind(this), }, { name: "wp_performance_export", description: "Export comprehensive performance report", parameters: [ { name: "site", type: "string", description: "Specific site ID for multi-site setups (optional for single site)", required: false, }, { name: "format", type: "string", description: "Export format (json, csv, summary)", required: false, }, { name: "includeHistorical", type: "boolean", description: "Include historical data (default: true)", required: false, }, { name: "includeAnalytics", type: "boolean", description: "Include analytics and insights (default: true)", required: false, }, { name: "timeRange", type: "string", description: "Time range for data export (1h, 6h, 24h, 7d, 30d)", required: false, }, ], handler: this.exportPerformanceReport.bind(this), }, ]; } /** * Get real-time performance statistics */ private async getPerformanceStats(_client: WordPressClient, params: Record): Promise { return toolWrapper(async () => { const { site, category = "overview", format = "summary", } = params as { site?: string; category?: string; format?: string }; // Get current metrics const metrics = this.collector.collectCurrentMetrics(); // What is breaching right now, from the same rules that raise alerts — health and alert status agree. const activeAlerts = this.collector.getActiveAlerts(); // Get site-specific metrics if requested let siteMetrics = null; if (site) { siteMetrics = this.collector.getSiteMetrics(site as string); } // Filter by category const result: Record = {}; if (category === "overview" || category === "all") { result.overview = { scope: site ? "session-wide (all sites combined)" : "session-wide", overallHealth: calculateHealthStatus(metrics, activeAlerts), alertStatus: calculateActiveAlertStatus(activeAlerts), activeAlerts: { total: activeAlerts.length, error: activeAlerts.filter((a) => a.severity === "error").length, warning: activeAlerts.filter((a) => a.severity === "warning").length, conditions: activeAlerts.map((a) => a.message), }, performanceScore: calculatePerformanceScore(metrics), totalRequests: metrics.requests.total, averageResponseTime: `${metrics.requests.averageResponseTime.toFixed(0)}ms`, cacheHitRate: formatHitRate(metrics.cache), // Server/network failures only; expected 4xx responses are reported separately. errorRate: `${((metrics.requests.failed / Math.max(metrics.requests.total, 1)) * 100).toFixed(2)}%`, clientErrors: metrics.requests.clientErrors, uptime: formatUptime(metrics.system.uptime), }; } if (category === "requests" || category === "all") { result.requests = { ...metrics.requests, requestsPerSecond: metrics.requests.requestsPerSecond.toFixed(2), p50ResponseTime: `${metrics.requests.p50ResponseTime}ms`, p95ResponseTime: `${metrics.requests.p95ResponseTime}ms`, p99ResponseTime: `${metrics.requests.p99ResponseTime}ms`, }; } if (category === "cache" || category === "all") { result.cache = { scope: CACHE_SCOPE_ALL_SITES, ...metrics.cache, hitRate: formatHitRate(metrics.cache), memoryUsage: `${metrics.cache.memoryUsageMB.toFixed(1)}MB`, efficiency: calculateCacheEfficiency(metrics.cache), }; } if (category === "system" || category === "all") { result.system = { ...metrics.system, memoryUsage: `${metrics.system.memoryUsage}% of V8 heap limit`, cpuUsage: `${metrics.system.cpuUsage}%`, uptime: formatUptime(metrics.system.uptime), }; } if (category === "tools" || category === "all") { result.tools = { mostUsedTool: metrics.tools.mostUsedTool, totalToolCalls: Object.values(metrics.tools.toolUsageCount).reduce( (sum: number, count: unknown) => sum + (typeof count === "number" ? count : 0), 0, ), topTools: Object.entries(metrics.tools.toolUsageCount) .sort(([, a], [, b]) => (b as number) - (a as number)) .slice(0, 5) .map(([tool, count]) => ({ tool, count })), toolPerformance: format === "detailed" ? metrics.tools.toolPerformance : undefined, // Newest first. Parameters are never recorded; errors carry their HTTP status and code. recentCalls: this.collector.getRecentToolCalls(format === "detailed" ? 50 : 10).map((call) => ({ ...call, timestamp: new Date(call.timestamp).toISOString(), })), }; } // Add site-specific data if requested if (siteMetrics && siteMetrics.isActive) { result.siteSpecific = { siteId: site, scope: `site ${site} only, since the server started`, cache: siteMetrics.cache && { ...siteMetrics.cache, hitRate: formatHitRate(siteMetrics.cache), }, client: siteMetrics.client, }; } // Add metadata result.metadata = { timestamp: new Date().toISOString(), category, format, site: site || "all", monitoringEnabled: true, }; return { success: true, data: result, }; }); } /** * Get historical performance data and trends */ private async getPerformanceHistory(_client: WordPressClient, params: Record): Promise { return toolWrapper(async () => { const { site, timeframe = "24h", metrics: requestedMetrics, includeTrends = true, } = params as { site?: string; timeframe?: string; metrics?: string[]; includeTrends?: boolean; }; // Convert timeframe to milliseconds const timeframMs = parseTimeframe(timeframe); const startTime = Date.now() - timeframMs; // Get historical data const historicalData = this.monitor.getHistoricalData(startTime); // Analyze trends if requested let trends = null; if (includeTrends) { // Add current data for trend analysis this.analytics.addDataPoint(this.collector.collectCurrentMetrics()); trends = this.analytics.analyzeTrends(); // Filter trends by requested metrics if (requestedMetrics && Array.isArray(requestedMetrics)) { trends = trends.filter((trend) => requestedMetrics.includes(trend.metric)); } } // Process historical data for charting const chartData = processHistoricalDataForChart(historicalData, requestedMetrics as string[] | undefined); return { success: true, data: { scope: { summary: `averages over the ${historicalData.length} snapshots taken in ${describeHistoryWindow(timeframe)}, ` + "all sites combined. Each snapshot's counters are totals since the server started, so these average " + "cumulative figures; totalRequests counts the requests between the first and last snapshot, and " + "averageCacheHitRate leaves out snapshots taken before any cache lookup (null if there are none)", historicalData: `each series is sampled to at most ${MAX_HISTORY_POINTS} points; dataPoints and the summary use every snapshot`, trends: ANALYTICS_SCOPE, }, timeframe, dataPoints: historicalData.length, historicalData: chartData, trends: trends || [], summary: { averageResponseTime: calculateAverage(historicalData.map((d) => d.requests.averageResponseTime)), // A snapshot before any cache lookup has no hit rate (hitRate 0 means "no data", not 0%). averageCacheHitRate: (() => { const rates = historicalData.filter((d) => hasCacheActivity(d.cache)).map((d) => d.cache.hitRate); return rates.length ? calculateAverage(rates) : null; })(), averageErrorRate: calculateAverage( historicalData.map((d) => (d.requests.total > 0 ? d.requests.failed / d.requests.total : 0)), ), // requests.total is cumulative in every snapshot: the difference is what happened in between. totalRequests: historicalData.length > 1 ? historicalData[historicalData.length - 1]!.requests.total - historicalData[0]!.requests.total : 0, }, metadata: { timestamp: new Date().toISOString(), site: site || "all", requestedMetrics: requestedMetrics || ["all"], }, }, }; }); } /** * Get benchmark comparison */ private async getBenchmarkComparison(_client: WordPressClient, params: Record): Promise { const start = Date.now(); this.logger.debug("performance benchmark: enter", { tool: "wp_performance_benchmark" }); let timer: ReturnType | undefined; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("wp_performance_benchmark timed out after 5000ms")), 5000); }); try { const result = await Promise.race([ toolWrapper(async () => { const { site, category = "all", includeRecommendations = true, } = params as { site?: string; category?: string; includeRecommendations?: boolean; }; // Get benchmark comparisons const benchmarks = this.analytics.benchmarkPerformance() as BenchmarkComparison[]; // Filter by category if specified let filteredBenchmarks = benchmarks; if (category !== "all") { const categoryMap: Record = { response_time: "Response Time", cache_performance: "Cache Hit Rate", error_rate: "Error Rate", system_resources: "Memory Usage", }; const targetCategory = categoryMap[category as string]; if (targetCategory) { filteredBenchmarks = benchmarks.filter((b) => b.category === targetCategory); } } // Get recommendations if requested let recommendations = null; if (includeRecommendations) { const insights = this.analytics.generateInsights(); recommendations = insights .filter((insight) => insight.category === "optimization") .map((insight) => ({ title: insight.title, description: insight.description, priority: insight.priority, estimatedImprovement: insight.estimatedImprovement, implementationEffort: insight.implementationEffort, })); } return { success: true, data: { benchmarks: filteredBenchmarks.map((benchmark) => ({ ...benchmark, status: formatBenchmarkStatus(benchmark.status), improvement: benchmark.improvement > 0 ? { needed: benchmark.improvement, description: getBenchmarkImprovementDescription(benchmark), } : null, })), overallRanking: calculateOverallRanking(benchmarks), recommendations: recommendations || [], metadata: { timestamp: new Date().toISOString(), category, site: site || "all", scope: CURRENT_METRICS_SCOPE, benchmarkVersion: "2024-industry-standards", }, }, }; }), timeoutPromise, ]); this.logger.debug("performance benchmark: exit", { tool: "wp_performance_benchmark", durationMs: Date.now() - start, }); return result; } catch (err) { this.logger.error("performance benchmark: failed", { tool: "wp_performance_benchmark", durationMs: Date.now() - start, error: err instanceof Error ? err.message : String(err), }); return { success: false, status: "unavailable", error: err instanceof Error ? err.message : String(err), data: { benchmarks: [], recommendations: [], metadata: { timestamp: new Date().toISOString() } }, }; } finally { if (timer !== undefined) clearTimeout(timer); } } /** * Get performance alerts and anomalies */ private async getPerformanceAlerts(_client: WordPressClient, params: Record): Promise { return toolWrapper(async () => { const { site, severity, category, limit = 20, includeAnomalies = true, } = params as { site?: string; severity?: string; category?: string; limit?: number; includeAnomalies?: boolean; }; // Get alerts from monitor let alerts = this.monitor.getAlerts(severity) as PerformanceAlert[]; // Filter by category if specified if (category) { alerts = alerts.filter((alert) => alert.category === category); } // Limit results alerts = alerts.slice(-(limit as number)); // Get anomalies if requested let anomalies: PerformanceAnomaly[] = []; if (includeAnomalies) { anomalies = this.analytics.getAnomalies(severity) as PerformanceAnomaly[]; } // Calculate alert summary const alertSummary = { total: alerts.length, critical: alerts.filter((a) => a.severity === "critical").length, error: alerts.filter((a) => a.severity === "error").length, warning: alerts.filter((a) => a.severity === "warning").length, info: alerts.filter((a) => a.severity === "info").length, }; // What is breaching right now. Unlike the recorded history below, it clears when a problem recovers. const activeAlerts = this.collector.getActiveAlerts(); const anomalySummary = { total: anomalies.length, critical: anomalies.filter((a) => a.severity === "critical").length, major: anomalies.filter((a) => a.severity === "major").length, moderate: anomalies.filter((a) => a.severity === "moderate").length, minor: anomalies.filter((a) => a.severity === "minor").length, }; const recordedAlerts = this.monitor.getAlerts() as PerformanceAlert[]; const recordedAnomalies = includeAnomalies ? (this.analytics.getAnomalies() as PerformanceAnomaly[]) : []; const historyStatus = calculateAlertStatus( { critical: recordedAlerts.filter((a) => a.severity === "critical").length, error: recordedAlerts.filter((a) => a.severity === "error").length, warning: recordedAlerts.filter((a) => a.severity === "warning").length, }, { critical: recordedAnomalies.filter((a) => a.severity === "critical").length, major: recordedAnomalies.filter((a) => a.severity === "major").length, moderate: recordedAnomalies.filter((a) => a.severity === "moderate").length, minor: recordedAnomalies.filter((a) => a.severity === "minor").length, }, ); return { success: true, data: { scope: { alerts: "recorded history, all sites combined: each message shows the value at the alert's timestamp (a repeat " + "within the 15-minute cooldown updates the same alert), so it can differ from the current figures", activeAlerts: "current metrics, all sites combined: what is breaching now", anomalies: "all sites combined: each anomaly compares a value with the recent snapshots when it was detected", }, alerts: alerts.map((alert) => ({ ...alert, timestamp: new Date(alert.timestamp).toISOString(), formattedMessage: formatAlertMessage(alert), })), anomalies: anomalies.map((anomaly) => ({ ...anomaly, timestamp: new Date(anomaly.timestamp).toISOString(), formattedDescription: formatAnomalyDescription(anomaly), })), summary: { alerts: alertSummary, anomalies: anomalySummary, // overallStatus covers the WHOLE recorded history (alerts never expire, and the severity/category/limit // filters above shape only the returned list), so it can stay red after a problem recovers; it is also // never better than currentStatus, which is the field that clears. overallStatus: worseAlertStatus(historyStatus, calculateActiveAlertStatus(activeAlerts)), currentStatus: calculateActiveAlertStatus(activeAlerts), activeAlerts: { total: activeAlerts.length, error: activeAlerts.filter((a) => a.severity === "error").length, warning: activeAlerts.filter((a) => a.severity === "warning").length, conditions: activeAlerts.map((a) => a.message), }, }, metadata: { timestamp: new Date().toISOString(), filters: { severity, category, site: site || "all" }, limit, }, }, }; }); } /** * Get optimization recommendations */ private async getOptimizationRecommendations( _client: WordPressClient, params: Record, ): Promise { return toolWrapper(async () => { const { site, focus = "speed", priority = "all", includeROI = true, includePredictions = true, } = params as { site?: string; focus?: string; priority?: string; includeROI?: boolean; includePredictions?: boolean; }; // Generate optimization plan const optimizationPlan = this.analytics.generateOptimizationPlan(); // Filter by priority let recommendations: Array<{ priority: string; impact: string; implementationEffort: string; [key: string]: unknown; }> = []; if (priority === "quick_wins" || priority === "all") { recommendations.push( ...optimizationPlan.quickWins.map((r) => ({ ...r, timeline: "quick_wins", })), ); } if (priority === "medium_term" || priority === "all") { recommendations.push( ...optimizationPlan.mediumTerm.map((r) => ({ ...r, timeline: "medium_term", })), ); } if (priority === "long_term" || priority === "all") { recommendations.push( ...optimizationPlan.longTerm.map((r) => ({ ...r, timeline: "long_term", })), ); } // Filter by focus area if (focus !== "speed") { const focusMap: Record = { reliability: ["reliability"], efficiency: ["cost", "performance"], scaling: ["performance", "reliability"], }; const targetImpacts = focusMap[focus] || []; recommendations = recommendations.filter((r) => targetImpacts.includes(r.impact)); } // Get predictions if requested let predictions: Record | null = null; if (includePredictions) { predictions = this.analytics.predictPerformance(60); // 1 hour prediction } return { success: true, data: { recommendations: recommendations.map((rec) => ({ ...rec, formattedPriority: formatPriority(rec.priority), formattedEffort: formatEffort(rec.implementationEffort), })), roi: includeROI ? optimizationPlan.estimatedROI : null, predictions: predictions || null, summary: { totalRecommendations: recommendations.length, quickWins: optimizationPlan.quickWins.length, mediumTerm: optimizationPlan.mediumTerm.length, longTerm: optimizationPlan.longTerm.length, estimatedImpact: calculateEstimatedImpact(recommendations), }, metadata: { timestamp: new Date().toISOString(), focus, priority, site: site || "all", scope: { recommendations: CURRENT_METRICS_SCOPE, predictions: ANALYTICS_SCOPE }, }, }, }; }); } /** * Export comprehensive performance report */ private async exportPerformanceReport(_client: WordPressClient, params: Record): Promise { return toolWrapper(async () => { const { site, format = "json", includeHistorical = true, includeAnalytics = true, timeRange = "24h", } = params as { site?: string; format?: string; includeHistorical?: boolean; includeAnalytics?: boolean; timeRange?: string; }; // Generate comprehensive analytics report const report = this.analytics.exportAnalyticsReport(); // Add additional data based on parameters const exportData: { currentMetrics: PerformanceMetrics; [key: string]: unknown; } = { metadata: { generatedAt: new Date().toISOString(), site: site || "all", timeRange, format, version: "1.0.0", }, summary: report.summary, currentMetrics: this.collector.collectCurrentMetrics(), }; if (includeHistorical) { const timeframMs = parseTimeframe(timeRange); const startTime = Date.now() - timeframMs; const history = this.monitor.getHistoricalData(startTime); // A day of snapshots is thousands of entries; export an even sample and say how many there were. exportData.historicalData = downsample(history, MAX_HISTORY_POINTS); exportData.historicalDataPoints = history.length; } if (includeAnalytics) { exportData.analytics = { trends: report.trends, benchmarks: report.benchmarks, insights: report.insights, anomalies: report.anomalies, predictions: report.predictions, optimizationPlan: report.optimizationPlan, }; } // Add aggregated statistics exportData.aggregatedStats = { cache: this.collector.getAggregatedCacheStats(), client: this.collector.getAggregatedClientStats(), }; // Add site comparison if multi-site if (!site) { exportData.siteComparison = this.collector.compareSitePerformance(); } // Format output based on requested format let formattedOutput: unknown; if (format === "csv") { formattedOutput = convertToCSV(exportData); } else if (format === "summary") { formattedOutput = createSummaryReport(exportData); } else { formattedOutput = exportData; } return { success: true, data: formattedOutput, metadata: { timestamp: new Date().toISOString(), format, dataSize: JSON.stringify(exportData).length, site: site || "all", // In the outer metadata so the CSV and summary formats carry it too; one entry per included section. scope: { currentMetrics: CURRENT_METRICS_SCOPE, aggregatedStats: CURRENT_METRICS_SCOPE, ...(exportData.siteComparison ? { siteComparison: "each site on its own, since the server started" } : {}), ...(includeHistorical ? { historicalData: `snapshots taken in ${describeHistoryWindow(timeRange)}, sampled to at most ${MAX_HISTORY_POINTS} points (historicalDataPoints counts them all); each one's counters are totals since the server started`, } : {}), ...(includeAnalytics ? { analytics: `trends, anomalies and predictions: ${ANALYTICS_SCOPE}; ` + `benchmarks, insights and optimizationPlan: ${CURRENT_METRICS_SCOPE}`, } : {}), }, }, }; }); } /** * Start historical data collection */ private startHistoricalDataCollection(): void { // Skip in test environments to prevent performance issues if (ConfigHelpers.isTest() || ConfigHelpers.isCI()) { this.logger.debug("Skipping historical data collection in test/CI environment"); return; } // Adjust collection frequency based on environment const interval = ConfigHelpers.isDev() ? 60000 : 30000; // 1 minute in dev, 30 seconds in prod this.logger.info("Starting historical data collection", { interval: `${interval / 1000}s`, environment: ConfigHelpers.get().get().app.nodeEnv, }); // unref() so this background timer alone can never keep a process (or a // one-shot script that merely imports/instantiates this class, e.g. // docs generation) alive. this.historicalDataInterval = setInterval(() => { try { const currentMetrics = this.collector.collectCurrentMetrics(); this.analytics.addDataPoint(currentMetrics); this.logger.debug("Historical metrics collected", { timestamp: new Date().toISOString(), }); } catch (_error) { this.logger.error("Failed to collect historical metrics", { _error: _error instanceof Error ? _error.message : String(_error), }); } }, interval).unref(); } /** * Stop historical data collection and cleanup resources */ public destroy(): void { if (this.historicalDataInterval) { clearInterval(this.historicalDataInterval); this.historicalDataInterval = undefined; this.logger.info("Historical data collection stopped"); } } }