/**
* Real-time Metrics Collector for WordPress MCP Server
* Integrates with existing client and cache systems
*/
import { PerformanceMonitor, PerformanceMetrics } from "./PerformanceMonitor.js";
import type { CacheStats } from "@/cache/CacheManager.js";
import type { ClientStats } from "@/types/client.js";
import { ConfigHelpers } from "@/config/Config.js";
import { LoggerFactory } from "@/utils/logger.js";
export interface CollectorConfig {
enableRealTime: boolean;
collectInterval: number;
enableToolTracking: boolean;
enableRequestInterception: boolean;
enableCacheIntegration: boolean;
enableSystemMetrics: boolean;
}
export interface RequestMetadata {
toolName?: string;
siteId?: string;
endpoint: string;
method: string;
startTime: number;
fromCache: boolean;
}
export interface ToolExecutionContext {
toolName: string;
parameters: Record<string, unknown>;
startTime: number;
siteId: string | undefined;
}
/**
* Metrics Collector - Central hub for all performance data
*/
export class MetricsCollector {
private monitor: PerformanceMonitor;
private config: CollectorConfig;
private activeRequests: Map<string, RequestMetadata> = new Map();
private activeTools: Map<string, ToolExecutionContext> = new Map();
private clientInstances: Map<string, unknown> = new Map();
private cacheManagers: Map<string, unknown> = new Map();
private logger = LoggerFactory.performance();
private realTimeInterval?: NodeJS.Timeout | undefined;
constructor(monitor: PerformanceMonitor, config: Partial<CollectorConfig> = {}) {
this.monitor = monitor;
this.config = {
enableRealTime: true,
collectInterval: 30000, // 30 seconds
enableToolTracking: true,
enableRequestInterception: true,
enableCacheIntegration: true,
enableSystemMetrics: true,
...config,
};
// Only enable real-time collection in appropriate environments
if (this.config.enableRealTime && (ConfigHelpers.isDev() || ConfigHelpers.isProd())) {
this.startRealTimeCollection();
} else if (ConfigHelpers.isTest() || ConfigHelpers.isCI()) {
this.logger.debug("Skipping real-time metrics collection in test/CI environment");
}
}
/**
* Register a WordPress client for monitoring
*/
registerClient(siteId: string, client: unknown): void {
this.clientInstances.set(siteId, client);
if (this.config.enableRequestInterception) {
this.interceptClientRequests(siteId, client as Record<string, unknown>);
}
}
/**
* Register a cache manager for monitoring
*/
registerCacheManager(siteId: string, cacheManager: unknown): void {
this.cacheManagers.set(siteId, cacheManager);
}
/**
* Start tracking a tool execution
*/
startToolExecution(toolName: string, parameters: Record<string, unknown>, siteId?: string): string {
const executionId = `${toolName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.activeTools.set(executionId, {
toolName,
parameters,
startTime: Date.now(),
siteId,
});
return executionId;
}
/**
* End tool execution and record metrics
*/
endToolExecution(executionId: string, success: boolean, error?: Error): void {
const context = this.activeTools.get(executionId);
if (!context) return;
const responseTime = Date.now() - context.startTime;
// Record in performance monitor
this.monitor.recordRequest(responseTime, success, context.toolName);
this.activeTools.delete(executionId);
}
/**
* Record a raw request (bypass tool tracking)
*/
recordRawRequest(responseTime: number, success: boolean, endpoint: string, fromCache: boolean = false): void {
this.monitor.recordRequest(responseTime, success);
}
/**
* Collect current metrics from all sources
*/
collectCurrentMetrics(): PerformanceMetrics {
// Update cache metrics from all registered cache managers
this.updateCacheMetrics();
// Update client metrics from all registered clients
this.updateClientMetrics();
// Get current metrics from monitor
return this.monitor.getMetrics();
}
/**
* Get aggregated cache statistics
*/
getAggregatedCacheStats(): CacheStats {
const aggregated: CacheStats = {
hits: 0,
misses: 0,
evictions: 0,
totalSize: 0,
hitRate: 0,
};
for (const [_siteId, cacheManager] of this.cacheManagers) {
const manager = cacheManager as { getStats?: () => CacheStats };
if (manager && typeof manager.getStats === "function") {
const stats = manager.getStats();
aggregated.hits += stats.hits || 0;
aggregated.misses += stats.misses || 0;
aggregated.evictions += stats.evictions || 0;
aggregated.totalSize += stats.totalSize || 0;
}
}
// Calculate overall hit rate
const total = aggregated.hits + aggregated.misses;
aggregated.hitRate = total > 0 ? aggregated.hits / total : 0;
return aggregated;
}
/**
* Get aggregated client statistics
*/
getAggregatedClientStats(): ClientStats {
const aggregated: ClientStats = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
rateLimitHits: 0,
authFailures: 0,
errors: 0,
};
const responseTimes: number[] = [];
for (const [_siteId, client] of this.clientInstances) {
const clientObj = client as { getStats?: () => ClientStats };
if (clientObj && typeof clientObj.getStats === "function") {
const stats = clientObj.getStats();
aggregated.totalRequests += stats.totalRequests || 0;
aggregated.successfulRequests += stats.successfulRequests || 0;
aggregated.failedRequests += stats.failedRequests || 0;
aggregated.rateLimitHits += stats.rateLimitHits || 0;
aggregated.authFailures += stats.authFailures || 0;
if (stats.averageResponseTime) {
responseTimes.push(stats.averageResponseTime);
}
}
}
// Calculate overall average response time
if (responseTimes.length > 0) {
aggregated.averageResponseTime = responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length;
}
return aggregated;
}
/**
* Get performance summary for specific site
*/
getSiteMetrics(siteId: string): {
cache?: CacheStats;
client?: ClientStats;
isActive: boolean;
} {
const result: Record<string, unknown> = { isActive: false };
const cacheManager = this.cacheManagers.get(siteId);
const cacheObj = cacheManager as { getStats?: () => CacheStats } | undefined;
if (cacheObj && typeof cacheObj.getStats === "function") {
result.cache = cacheObj.getStats();
result.isActive = true;
}
const client = this.clientInstances.get(siteId);
const clientObj = client as { getStats?: () => ClientStats } | undefined;
if (clientObj && typeof clientObj.getStats === "function") {
result.client = clientObj.getStats();
result.isActive = true;
}
return result as { cache?: CacheStats; client?: ClientStats; isActive: boolean };
}
/**
* Generate performance comparison between sites
*/
compareSitePerformance(): {
sites: string[];
comparison: Record<
string,
{
responseTime: number;
cacheHitRate: number;
errorRate: number;
requestCount: number;
ranking: number;
}
>;
bestPerforming: string;
worstPerforming: string;
} {
const sites = Array.from(this.clientInstances.keys());
const comparison: Record<
string,
{
responseTime: number;
cacheHitRate: number;
errorRate: number;
requestCount: number;
ranking: number;
}
> = {};
const rankings: Array<{ site: string; score: number }> = [];
for (const siteId of sites) {
const metrics = this.getSiteMetrics(siteId);
const responseTime = metrics.client?.averageResponseTime || 0;
const cacheHitRate = metrics.cache?.hitRate || 0;
const errorRate = metrics.client ? metrics.client.failedRequests / Math.max(metrics.client.totalRequests, 1) : 0;
const requestCount = metrics.client?.totalRequests || 0;
// Calculate performance score (lower is better for response time and error rate)
const score = responseTime / 1000 + errorRate * 100 - cacheHitRate * 50 + requestCount * 0.001;
comparison[siteId] = {
responseTime,
cacheHitRate,
errorRate,
requestCount,
ranking: 0, // Will be set after sorting
};
rankings.push({ site: siteId, score });
}
// Sort by performance score
rankings.sort((a, b) => a.score - b.score);
// Assign rankings
rankings.forEach((item, index) => {
const siteData = comparison[item.site];
if (siteData) {
siteData.ranking = index + 1;
}
});
return {
sites,
comparison,
bestPerforming: rankings[0]?.site || "",
worstPerforming: rankings[rankings.length - 1]?.site || "",
};
}
/**
* Generate optimization suggestions
*/
generateOptimizationSuggestions(): {
critical: string[];
recommended: string[];
optional: string[];
} {
const metrics = this.collectCurrentMetrics();
const critical: string[] = [];
const recommended: string[] = [];
const optional: string[] = [];
// Critical issues
if (metrics.requests.averageResponseTime > 5000) {
critical.push("Response times are critically high (>5s). Enable caching immediately.");
}
const errorRate = metrics.requests.failed / Math.max(metrics.requests.total, 1);
if (errorRate > 0.1) {
critical.push("Error rate is critically high (>10%). Check WordPress connectivity.");
}
// Recommended optimizations
if (metrics.cache.hitRate < 0.8) {
recommended.push("Cache hit rate is below 80%. Consider cache warming or TTL adjustment.");
}
if (metrics.requests.averageResponseTime > 2000) {
recommended.push("Response times could be improved. Consider enabling more aggressive caching.");
}
if (metrics.system.memoryUsage > 80) {
recommended.push("Memory usage is high. Consider increasing cache size limits or server resources.");
}
// Optional optimizations
if (metrics.cache.totalSize < 100) {
optional.push("Cache utilization is low. Consider pre-warming with frequently accessed data.");
}
if (Object.keys(metrics.tools.toolUsageCount).length > 10) {
optional.push("Many tools are being used. Consider creating custom workflows for common operations.");
}
return { critical, recommended, optional };
}
/**
* Export detailed performance report
*/
exportDetailedReport(): {
timestamp: string;
overview: PerformanceMetrics;
siteComparison: Record<string, unknown>;
aggregatedStats: {
cache: CacheStats;
client: ClientStats;
};
optimizations: Record<string, unknown>;
alerts: unknown[];
} {
return {
timestamp: new Date().toISOString(),
overview: this.collectCurrentMetrics(),
siteComparison: this.compareSitePerformance(),
aggregatedStats: {
cache: this.getAggregatedCacheStats(),
client: this.getAggregatedClientStats(),
},
optimizations: this.generateOptimizationSuggestions(),
alerts: this.monitor.getAlerts(),
};
}
/**
* Start real-time metric collection
*/
private startRealTimeCollection(): void {
// Skip in test/CI environments to prevent performance issues
if (ConfigHelpers.isTest() || ConfigHelpers.isCI()) {
return;
}
// Adjust collection frequency based on environment
const interval = ConfigHelpers.isDev()
? Math.max(this.config.collectInterval * 2, 60000) // Longer intervals in dev
: this.config.collectInterval;
this.logger.info("Starting real-time metrics collection", {
interval: `${interval / 1000}s`,
environment: ConfigHelpers.get().get().app.nodeEnv,
});
this.realTimeInterval = setInterval(() => {
try {
this.updateCacheMetrics();
this.updateClientMetrics();
this.logger.debug("Real-time metrics updated");
} catch (_error) {
this.logger.error("Failed to update real-time metrics", {
_error: _error instanceof Error ? _error.message : String(_error),
});
}
}, interval);
}
/**
* Stop real-time collection and cleanup resources
*/
public destroy(): void {
if (this.realTimeInterval) {
clearInterval(this.realTimeInterval);
this.realTimeInterval = undefined;
this.logger.info("Real-time metrics collection stopped");
}
}
/**
* Update cache metrics in performance monitor
*/
private updateCacheMetrics(): void {
const aggregatedStats = this.getAggregatedCacheStats();
// Type assertion: convert CacheStats to Record<string, unknown> via unknown
this.monitor.updateCacheMetrics(aggregatedStats as unknown as Record<string, unknown>);
}
/**
* Update client metrics in performance monitor
*/
private updateClientMetrics(): void {
// Client metrics are updated through request interception
// This method can be used for additional client-specific metrics
}
/**
* Intercept client requests for automatic tracking
*/
private interceptClientRequests(siteId: string, client: Record<string, unknown>): void {
if (!client.request || typeof client.request !== "function") {
return;
}
const originalRequest = client.request.bind(client);
const clientObj = client as {
request?: (...args: unknown[]) => Promise<unknown>;
originalRequest?: (...args: unknown[]) => Promise<unknown>;
};
if (!clientObj.request) return;
clientObj.originalRequest = clientObj.request;
clientObj.request = async (...args: unknown[]) => {
const startTime = Date.now();
const requestId = `${siteId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Extract metadata with proper type assertions
const metadata: RequestMetadata = {
siteId,
endpoint: (args[0] as string) || "unknown",
method: (args[1] as string) || "GET",
startTime,
fromCache: false,
};
this.activeRequests.set(requestId, metadata);
try {
const result = await originalRequest(...args);
const responseTime = Date.now() - startTime;
// Check if response came from cache
const _fromCache = result.cached || false;
this.monitor.recordRequest(responseTime, true);
this.activeRequests.delete(requestId);
return result;
} catch (_error) {
const responseTime = Date.now() - startTime;
this.monitor.recordRequest(responseTime, false);
this.activeRequests.delete(requestId);
throw _error;
}
};
}
/**
* Stop all monitoring and cleanup
*/
stop(): void {
this.monitor.stop();
this.activeRequests.clear();
this.activeTools.clear();
}
}