import * as plugins from '../plugins.js'; import { DcRouter } from '../classes.dcrouter.js'; export class MetricsManager { private logger: plugins.smartlog.Smartlog; private smartMetrics: plugins.smartmetrics.SmartMetrics; private dcRouter: DcRouter; private resetInterval?: NodeJS.Timeout; // Constants private readonly MAX_TOP_DOMAINS = 1000; // Limit topDomains Map size // Track email-specific metrics private emailMetrics = { sentToday: 0, receivedToday: 0, failedToday: 0, bouncedToday: 0, queueSize: 0, lastResetDate: new Date().toDateString(), deliveryTimes: [] as number[], // Track delivery times in ms recipients: new Map(), // Track email count by recipient recentActivity: [] as Array<{ timestamp: number; type: string; details: string }>, }; // Track DNS-specific metrics private dnsMetrics = { totalQueries: 0, cacheHits: 0, cacheMisses: 0, queryTypes: {} as Record, topDomains: new Map(), lastResetDate: new Date().toDateString(), queryTimestamps: [] as number[], // Track query timestamps for rate calculation responseTimes: [] as number[], // Track response times in ms }; // Track security-specific metrics private securityMetrics = { blockedIPs: 0, authFailures: 0, spamDetected: 0, malwareDetected: 0, phishingDetected: 0, lastResetDate: new Date().toDateString(), incidents: [] as Array<{ timestamp: number; type: string; severity: string; details: string }>, }; constructor(dcRouter: DcRouter) { this.dcRouter = dcRouter; // Create a new Smartlog instance for metrics this.logger = new plugins.smartlog.Smartlog({ logContext: { environment: 'production', runtime: 'node', zone: 'dcrouter-metrics', } }); this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter'); } public async start(): Promise { // Start SmartMetrics collection this.smartMetrics.start(); // Reset daily counters at midnight this.resetInterval = setInterval(() => { const currentDate = new Date().toDateString(); if (currentDate !== this.emailMetrics.lastResetDate) { this.emailMetrics.sentToday = 0; this.emailMetrics.receivedToday = 0; this.emailMetrics.failedToday = 0; this.emailMetrics.bouncedToday = 0; this.emailMetrics.deliveryTimes = []; this.emailMetrics.recipients.clear(); this.emailMetrics.recentActivity = []; this.emailMetrics.lastResetDate = currentDate; } if (currentDate !== this.dnsMetrics.lastResetDate) { this.dnsMetrics.totalQueries = 0; this.dnsMetrics.cacheHits = 0; this.dnsMetrics.cacheMisses = 0; this.dnsMetrics.queryTypes = {}; this.dnsMetrics.topDomains.clear(); this.dnsMetrics.queryTimestamps = []; this.dnsMetrics.responseTimes = []; this.dnsMetrics.lastResetDate = currentDate; } if (currentDate !== this.securityMetrics.lastResetDate) { this.securityMetrics.blockedIPs = 0; this.securityMetrics.authFailures = 0; this.securityMetrics.spamDetected = 0; this.securityMetrics.malwareDetected = 0; this.securityMetrics.phishingDetected = 0; this.securityMetrics.incidents = []; this.securityMetrics.lastResetDate = currentDate; } }, 60000); // Check every minute this.logger.log('info', 'MetricsManager started'); } public async stop(): Promise { // Clear the reset interval if (this.resetInterval) { clearInterval(this.resetInterval); this.resetInterval = undefined; } this.smartMetrics.stop(); this.logger.log('info', 'MetricsManager stopped'); } // Get server metrics from SmartMetrics and SmartProxy public async getServerStats() { const smartMetricsData = await this.smartMetrics.getMetrics(); const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStatistics() : null; return { uptime: process.uptime(), startTime: Date.now() - (process.uptime() * 1000), memoryUsage: { heapUsed: process.memoryUsage().heapUsed, heapTotal: process.memoryUsage().heapTotal, external: process.memoryUsage().external, rss: process.memoryUsage().rss, // Add SmartMetrics memory data maxMemoryMB: this.smartMetrics.maxMemoryMB, actualUsageBytes: smartMetricsData.memoryUsageBytes, actualUsagePercentage: smartMetricsData.memoryPercentage, }, cpuUsage: { user: parseFloat(smartMetricsData.cpuUsageText || '0'), system: 0, // SmartMetrics doesn't separate user/system }, activeConnections: proxyStats ? proxyStats.activeConnections : 0, totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0, requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0, throughput: proxyMetrics ? { bytesIn: proxyMetrics.totals.bytesIn(), bytesOut: proxyMetrics.totals.bytesOut() } : { bytesIn: 0, bytesOut: 0 }, }; } // Get email metrics public async getEmailStats() { // Calculate average delivery time const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0 ? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length : 0; // Get top recipients const topRecipients = Array.from(this.emailMetrics.recipients.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([email, count]) => ({ email, count })); // Get recent activity (last 50 entries) const recentActivity = this.emailMetrics.recentActivity.slice(-50); return { sentToday: this.emailMetrics.sentToday, receivedToday: this.emailMetrics.receivedToday, failedToday: this.emailMetrics.failedToday, bounceRate: this.emailMetrics.bouncedToday > 0 ? (this.emailMetrics.bouncedToday / this.emailMetrics.sentToday) * 100 : 0, deliveryRate: this.emailMetrics.sentToday > 0 ? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100 : 100, queueSize: this.emailMetrics.queueSize, averageDeliveryTime: Math.round(avgDeliveryTime), topRecipients, recentActivity, }; } // Get DNS metrics public async getDnsStats() { const cacheHitRate = this.dnsMetrics.totalQueries > 0 ? (this.dnsMetrics.cacheHits / this.dnsMetrics.totalQueries) * 100 : 0; const topDomains = Array.from(this.dnsMetrics.topDomains.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([domain, count]) => ({ domain, count })); // Calculate queries per second from recent timestamps const now = Date.now(); const oneMinuteAgo = now - 60000; const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo); const queriesPerSecond = recentQueries.length / 60; // Calculate average response time const avgResponseTime = this.dnsMetrics.responseTimes.length > 0 ? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length : 0; return { queriesPerSecond: Math.round(queriesPerSecond * 10) / 10, totalQueries: this.dnsMetrics.totalQueries, cacheHits: this.dnsMetrics.cacheHits, cacheMisses: this.dnsMetrics.cacheMisses, cacheHitRate: cacheHitRate, topDomains: topDomains, queryTypes: this.dnsMetrics.queryTypes, averageResponseTime: Math.round(avgResponseTime), activeDomains: this.dnsMetrics.topDomains.size, }; } // Get security metrics public async getSecurityStats() { // Get recent incidents (last 20) const recentIncidents = this.securityMetrics.incidents.slice(-20); return { blockedIPs: this.securityMetrics.blockedIPs, authFailures: this.securityMetrics.authFailures, spamDetected: this.securityMetrics.spamDetected, malwareDetected: this.securityMetrics.malwareDetected, phishingDetected: this.securityMetrics.phishingDetected, totalThreatsBlocked: this.securityMetrics.spamDetected + this.securityMetrics.malwareDetected + this.securityMetrics.phishingDetected, recentIncidents, }; } // Get connection info from SmartProxy public async getConnectionInfo() { const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; if (!proxyMetrics) { return []; } const connectionsByRoute = proxyMetrics.connections.byRoute(); const connectionInfo = []; for (const [routeName, count] of connectionsByRoute) { connectionInfo.push({ type: 'https', count, source: routeName, lastActivity: new Date(), }); } return connectionInfo; } // Email event tracking methods public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void { this.emailMetrics.sentToday++; if (recipient) { const count = this.emailMetrics.recipients.get(recipient) || 0; this.emailMetrics.recipients.set(recipient, count + 1); } if (deliveryTimeMs) { this.emailMetrics.deliveryTimes.push(deliveryTimeMs); // Keep only last 1000 delivery times if (this.emailMetrics.deliveryTimes.length > 1000) { this.emailMetrics.deliveryTimes.shift(); } } this.emailMetrics.recentActivity.push({ timestamp: Date.now(), type: 'sent', details: recipient || 'unknown', }); // Keep only last 1000 activities if (this.emailMetrics.recentActivity.length > 1000) { this.emailMetrics.recentActivity.shift(); } } public trackEmailReceived(sender?: string): void { this.emailMetrics.receivedToday++; this.emailMetrics.recentActivity.push({ timestamp: Date.now(), type: 'received', details: sender || 'unknown', }); // Keep only last 1000 activities if (this.emailMetrics.recentActivity.length > 1000) { this.emailMetrics.recentActivity.shift(); } } public trackEmailFailed(recipient?: string, reason?: string): void { this.emailMetrics.failedToday++; this.emailMetrics.recentActivity.push({ timestamp: Date.now(), type: 'failed', details: `${recipient || 'unknown'}: ${reason || 'unknown error'}`, }); // Keep only last 1000 activities if (this.emailMetrics.recentActivity.length > 1000) { this.emailMetrics.recentActivity.shift(); } } public trackEmailBounced(recipient?: string): void { this.emailMetrics.bouncedToday++; this.emailMetrics.recentActivity.push({ timestamp: Date.now(), type: 'bounced', details: recipient || 'unknown', }); // Keep only last 1000 activities if (this.emailMetrics.recentActivity.length > 1000) { this.emailMetrics.recentActivity.shift(); } } public updateQueueSize(size: number): void { this.emailMetrics.queueSize = size; } // DNS event tracking methods public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void { this.dnsMetrics.totalQueries++; if (cacheHit) { this.dnsMetrics.cacheHits++; } else { this.dnsMetrics.cacheMisses++; } // Track query timestamp this.dnsMetrics.queryTimestamps.push(Date.now()); // Keep only timestamps from last 5 minutes const fiveMinutesAgo = Date.now() - 300000; this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo); // Track response time if provided if (responseTimeMs) { this.dnsMetrics.responseTimes.push(responseTimeMs); // Keep only last 1000 response times if (this.dnsMetrics.responseTimes.length > 1000) { this.dnsMetrics.responseTimes.shift(); } } // Track query types this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1; // Track top domains with size limit const currentCount = this.dnsMetrics.topDomains.get(domain) || 0; this.dnsMetrics.topDomains.set(domain, currentCount + 1); // If we've exceeded the limit, remove the least accessed domains if (this.dnsMetrics.topDomains.size > this.MAX_TOP_DOMAINS) { // Convert to array, sort by count, and keep only top domains const sortedDomains = Array.from(this.dnsMetrics.topDomains.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8)); // Keep 80% to avoid frequent cleanup // Clear and repopulate with top domains this.dnsMetrics.topDomains.clear(); sortedDomains.forEach(([domain, count]) => { this.dnsMetrics.topDomains.set(domain, count); }); } } // Security event tracking methods public trackBlockedIP(ip?: string, reason?: string): void { this.securityMetrics.blockedIPs++; this.securityMetrics.incidents.push({ timestamp: Date.now(), type: 'ip_blocked', severity: 'medium', details: `IP ${ip || 'unknown'} blocked: ${reason || 'security policy'}`, }); // Keep only last 1000 incidents if (this.securityMetrics.incidents.length > 1000) { this.securityMetrics.incidents.shift(); } } public trackAuthFailure(username?: string, ip?: string): void { this.securityMetrics.authFailures++; this.securityMetrics.incidents.push({ timestamp: Date.now(), type: 'auth_failure', severity: 'low', details: `Authentication failed for ${username || 'unknown'} from ${ip || 'unknown'}`, }); // Keep only last 1000 incidents if (this.securityMetrics.incidents.length > 1000) { this.securityMetrics.incidents.shift(); } } public trackSpamDetected(sender?: string): void { this.securityMetrics.spamDetected++; this.securityMetrics.incidents.push({ timestamp: Date.now(), type: 'spam_detected', severity: 'low', details: `Spam detected from ${sender || 'unknown'}`, }); // Keep only last 1000 incidents if (this.securityMetrics.incidents.length > 1000) { this.securityMetrics.incidents.shift(); } } public trackMalwareDetected(source?: string): void { this.securityMetrics.malwareDetected++; this.securityMetrics.incidents.push({ timestamp: Date.now(), type: 'malware_detected', severity: 'high', details: `Malware detected from ${source || 'unknown'}`, }); // Keep only last 1000 incidents if (this.securityMetrics.incidents.length > 1000) { this.securityMetrics.incidents.shift(); } } public trackPhishingDetected(source?: string): void { this.securityMetrics.phishingDetected++; this.securityMetrics.incidents.push({ timestamp: Date.now(), type: 'phishing_detected', severity: 'high', details: `Phishing attempt from ${source || 'unknown'}`, }); // Keep only last 1000 incidents if (this.securityMetrics.incidents.length > 1000) { this.securityMetrics.incidents.shift(); } } // Get network metrics from SmartProxy public async getNetworkStats() { const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; if (!proxyMetrics) { return { connectionsByIP: new Map(), throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [], totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, }; } // Get metrics using the new API const connectionsByIP = proxyMetrics.connections.byIP(); const instantThroughput = proxyMetrics.throughput.instant(); // Get throughput rate const throughputRate = { bytesInPerSecond: instantThroughput.in, bytesOutPerSecond: instantThroughput.out }; // Get top IPs const topIPs = proxyMetrics.connections.topIPs(10); // Get total data transferred const totalDataTransferred = { bytesIn: proxyMetrics.totals.bytesIn(), bytesOut: proxyMetrics.totals.bytesOut() }; return { connectionsByIP, throughputRate, topIPs, totalDataTransferred, }; } }