import * as plugins from '../plugins.js'; import { DcRouter } from '../classes.dcrouter.js'; import { MetricsCache } from './classes.metricscache.js'; import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js'; export class MetricsManager { private logger: plugins.smartlog.Smartlog; private smartMetrics: plugins.smartmetrics.SmartMetrics; private dcRouter: DcRouter; private resetInterval?: NodeJS.Timeout; private metricsCache: MetricsCache; // 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 recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>, }; // Per-minute time-series buckets for charts private emailMinuteBuckets = new Map(); private dnsMinuteBuckets = new Map(); // 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'); // Initialize metrics cache with 500ms TTL this.metricsCache = new MetricsCache(500); } 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.recentQueries = []; 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() { return this.metricsCache.get('serverStats', async () => { const smartMetricsData = await this.smartMetrics.getMetrics(); const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; const proxyStats = this.dcRouter.smartProxy ? await 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: smartMetricsData.cpuPercentage, system: 0, }, 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(), bytesInPerSecond: proxyMetrics.throughput.instant().in, bytesOutPerSecond: proxyMetrics.throughput.instant().out, } : { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 }, }; }); } // Get email metrics public async getEmailStats() { return this.metricsCache.get('emailStats', () => { // 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() { return this.metricsCache.get('dnsStats', () => { 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, recentQueries: this.dnsMetrics.recentQueries.slice(), }; }); } /** * Sync security metrics from the SecurityLogger singleton (last 24h). * Called before returning security stats so counters reflect real events. */ private syncFromSecurityLogger(): void { try { const securityLogger = SecurityLogger.getInstance(); const summary = securityLogger.getEventsSummary(86400000); // last 24h this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0; this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0; this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC this.securityMetrics.authFailures = summary.byType[SecurityEventType.AUTHENTICATION] || 0; this.securityMetrics.blockedIPs = (summary.byType[SecurityEventType.IP_REPUTATION] || 0) + (summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0); } catch { // SecurityLogger may not be initialized yet — ignore } } // Get security metrics public async getSecurityStats() { return this.metricsCache.get('securityStats', () => { // Sync counters from the real SecurityLogger events this.syncFromSecurityLogger(); // 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() { return this.metricsCache.get('connectionInfo', () => { 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++; this.incrementEmailBucket('sent'); if (recipient) { const count = this.emailMetrics.recipients.get(recipient) || 0; this.emailMetrics.recipients.set(recipient, count + 1); // Cap recipients map to prevent unbounded growth within a day if (this.emailMetrics.recipients.size > this.MAX_TOP_DOMAINS) { const sorted = Array.from(this.emailMetrics.recipients.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8)); this.emailMetrics.recipients = new Map(sorted); } } 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.incrementEmailBucket('received'); 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.incrementEmailBucket('failed'); 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, answered?: boolean): void { this.dnsMetrics.totalQueries++; this.incrementDnsBucket(); // Store recent query entry this.dnsMetrics.recentQueries.push({ timestamp: Date.now(), domain, type: queryType, answered: answered ?? true, responseTimeMs: responseTimeMs ?? 0, }); if (this.dnsMetrics.recentQueries.length > 100) { this.dnsMetrics.recentQueries.shift(); } 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() { // Use shorter cache TTL for network stats to ensure real-time updates return this.metricsCache.get('networkStats', () => { const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; if (!proxyMetrics) { return { connectionsByIP: new Map(), throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [] as Array<{ ip: string; count: number }>, totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>, throughputByIP: new Map(), requestsPerSecond: 0, requestsTotal: 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() }; // Get throughput history from Rust engine (up to 300 seconds) const throughputHistory = proxyMetrics.throughput.history(300); // Get per-IP throughput const throughputByIP = proxyMetrics.throughput.byIP(); // Get HTTP request rates const requestsPerSecond = proxyMetrics.requests.perSecond(); const requestsTotal = proxyMetrics.requests.total(); return { connectionsByIP, throughputRate, topIPs, totalDataTransferred, throughputHistory, throughputByIP, requestsPerSecond, requestsTotal, }; }, 200); // Use 200ms cache for more frequent updates } // --- Time-series helpers --- private static minuteKey(ts: number = Date.now()): number { return Math.floor(ts / 60000) * 60000; } private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void { const key = MetricsManager.minuteKey(); let bucket = this.emailMinuteBuckets.get(key); if (!bucket) { bucket = { sent: 0, received: 0, failed: 0 }; this.emailMinuteBuckets.set(key, bucket); } bucket[field]++; } private incrementDnsBucket(): void { const key = MetricsManager.minuteKey(); let bucket = this.dnsMinuteBuckets.get(key); if (!bucket) { bucket = { queries: 0 }; this.dnsMinuteBuckets.set(key, bucket); } bucket.queries++; } private pruneOldBuckets(): void { const cutoff = Date.now() - 86400000; // 24h for (const key of this.emailMinuteBuckets.keys()) { if (key < cutoff) this.emailMinuteBuckets.delete(key); } for (const key of this.dnsMinuteBuckets.keys()) { if (key < cutoff) this.dnsMinuteBuckets.delete(key); } } /** * Get email time-series data for the last N hours, aggregated per minute. */ public getEmailTimeSeries(hours: number = 24): { sent: Array<{ timestamp: number; value: number }>; received: Array<{ timestamp: number; value: number }>; failed: Array<{ timestamp: number; value: number }>; } { this.pruneOldBuckets(); const cutoff = Date.now() - hours * 3600000; const sent: Array<{ timestamp: number; value: number }> = []; const received: Array<{ timestamp: number; value: number }> = []; const failed: Array<{ timestamp: number; value: number }> = []; const sortedKeys = Array.from(this.emailMinuteBuckets.keys()) .filter((k) => k >= cutoff) .sort((a, b) => a - b); for (const key of sortedKeys) { const bucket = this.emailMinuteBuckets.get(key)!; sent.push({ timestamp: key, value: bucket.sent }); received.push({ timestamp: key, value: bucket.received }); failed.push({ timestamp: key, value: bucket.failed }); } return { sent, received, failed }; } /** * Get DNS time-series data for the last N hours, aggregated per minute. */ public getDnsTimeSeries(hours: number = 24): { queries: Array<{ timestamp: number; value: number }>; } { this.pruneOldBuckets(); const cutoff = Date.now() - hours * 3600000; const queries: Array<{ timestamp: number; value: number }> = []; const sortedKeys = Array.from(this.dnsMinuteBuckets.keys()) .filter((k) => k >= cutoff) .sort((a, b) => a - b); for (const key of sortedKeys) { const bucket = this.dnsMinuteBuckets.get(key)!; queries.push({ timestamp: key, value: bucket.queries }); } return { queries }; } }