/** * Adaptive SMTP Logging System * Automatically switches between logging modes based on server load (active connections) * to maintain performance during high-concurrency scenarios */ import * as plugins from '../../../../plugins.js'; import { logger } from '../../../../logger.js'; import { SecurityLogLevel, SecurityEventType } from '../constants.js'; import type { ISmtpSession } from '../interfaces.js'; import type { LogLevel, ISmtpLogOptions } from './logging.js'; /** * Log modes based on server load */ export enum LogMode { VERBOSE = 'VERBOSE', // < 20 connections: Full detailed logging REDUCED = 'REDUCED', // 20-40 connections: Limited command/response logging, full error logging MINIMAL = 'MINIMAL' // 40+ connections: Aggregated logging only, critical errors only } /** * Configuration for adaptive logging thresholds */ export interface IAdaptiveLogConfig { verboseThreshold: number; // Switch to REDUCED mode above this connection count reducedThreshold: number; // Switch to MINIMAL mode above this connection count aggregationInterval: number; // How often to flush aggregated logs (ms) maxAggregatedEntries: number; // Max entries to hold before forced flush } /** * Aggregated log entry for batching similar events */ interface IAggregatedLogEntry { type: 'connection' | 'command' | 'response' | 'error'; count: number; firstSeen: number; lastSeen: number; sample: { message: string; level: LogLevel; options?: ISmtpLogOptions; }; } /** * Connection metadata for aggregation tracking */ interface IConnectionTracker { activeConnections: number; peakConnections: number; totalConnections: number; connectionsPerSecond: number; lastConnectionTime: number; } /** * Adaptive SMTP Logger that scales logging based on server load */ export class AdaptiveSmtpLogger { private static instance: AdaptiveSmtpLogger; private currentMode: LogMode = LogMode.VERBOSE; private config: IAdaptiveLogConfig; private aggregatedEntries: Map = new Map(); private aggregationTimer: NodeJS.Timeout | null = null; private connectionTracker: IConnectionTracker = { activeConnections: 0, peakConnections: 0, totalConnections: 0, connectionsPerSecond: 0, lastConnectionTime: Date.now() }; private constructor(config?: Partial) { this.config = { verboseThreshold: 20, reducedThreshold: 40, aggregationInterval: 30000, // 30 seconds maxAggregatedEntries: 100, ...config }; this.startAggregationTimer(); } /** * Get singleton instance */ public static getInstance(config?: Partial): AdaptiveSmtpLogger { if (!AdaptiveSmtpLogger.instance) { AdaptiveSmtpLogger.instance = new AdaptiveSmtpLogger(config); } return AdaptiveSmtpLogger.instance; } /** * Update active connection count and adjust log mode if needed */ public updateConnectionCount(activeConnections: number): void { this.connectionTracker.activeConnections = activeConnections; this.connectionTracker.peakConnections = Math.max( this.connectionTracker.peakConnections, activeConnections ); const newMode = this.determineLogMode(activeConnections); if (newMode !== this.currentMode) { this.switchLogMode(newMode); } } /** * Track new connection for rate calculation */ public trackConnection(): void { this.connectionTracker.totalConnections++; const now = Date.now(); const timeDiff = (now - this.connectionTracker.lastConnectionTime) / 1000; if (timeDiff > 0) { this.connectionTracker.connectionsPerSecond = 1 / timeDiff; } this.connectionTracker.lastConnectionTime = now; } /** * Get current logging mode */ public getCurrentMode(): LogMode { return this.currentMode; } /** * Get connection statistics */ public getConnectionStats(): IConnectionTracker { return { ...this.connectionTracker }; } /** * Log a message with adaptive behavior */ public log(level: LogLevel, message: string, options: ISmtpLogOptions = {}): void { // Always log structured data const errorInfo = options.error ? { errorMessage: options.error.message, errorStack: options.error.stack, errorName: options.error.name } : {}; const logData = { component: 'smtp-server', logMode: this.currentMode, activeConnections: this.connectionTracker.activeConnections, ...options, ...errorInfo }; if (logData.error) { delete logData.error; } logger.log(level, message, logData); // Adaptive console logging based on mode switch (this.currentMode) { case LogMode.VERBOSE: // Full console logging if (level === 'error' || level === 'warn') { console[level](`[SMTP] ${message}`, logData); } break; case LogMode.REDUCED: // Only errors and warnings to console if (level === 'error' || level === 'warn') { console[level](`[SMTP] ${message}`, logData); } break; case LogMode.MINIMAL: // Only critical errors to console if (level === 'error' && (message.includes('critical') || message.includes('security') || message.includes('crash'))) { console[level](`[SMTP] ${message}`, logData); } break; } } /** * Log command with adaptive behavior */ public logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void { const clientInfo = { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, secure: socket instanceof plugins.tls.TLSSocket, sessionId: session?.id, sessionState: session?.state }; switch (this.currentMode) { case LogMode.VERBOSE: this.log('info', `Command received: ${command}`, { ...clientInfo, command: command.split(' ')[0]?.toUpperCase() }); console.log(`← ${command}`); break; case LogMode.REDUCED: // Aggregate commands instead of logging each one this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); // Only show error commands if (command.toUpperCase().startsWith('QUIT') || command.includes('error')) { console.log(`← ${command}`); } break; case LogMode.MINIMAL: // Only aggregate, no console output unless it's an error command this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); break; } } /** * Log response with adaptive behavior */ public logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void { const clientInfo = { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, secure: socket instanceof plugins.tls.TLSSocket }; const responseCode = response.substring(0, 3); const isError = responseCode.startsWith('4') || responseCode.startsWith('5'); switch (this.currentMode) { case LogMode.VERBOSE: if (responseCode.startsWith('2') || responseCode.startsWith('3')) { this.log('debug', `Response sent: ${response}`, clientInfo); } else if (responseCode.startsWith('4')) { this.log('warn', `Temporary error response: ${response}`, clientInfo); } else if (responseCode.startsWith('5')) { this.log('error', `Permanent error response: ${response}`, clientInfo); } console.log(`→ ${response}`); break; case LogMode.REDUCED: // Log errors normally, aggregate success responses if (isError) { if (responseCode.startsWith('4')) { this.log('warn', `Temporary error response: ${response}`, clientInfo); } else { this.log('error', `Permanent error response: ${response}`, clientInfo); } console.log(`→ ${response}`); } else { this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); } break; case LogMode.MINIMAL: // Only log critical errors if (responseCode.startsWith('5')) { this.log('error', `Permanent error response: ${response}`, clientInfo); console.log(`→ ${response}`); } else { this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); } break; } } /** * Log connection event with adaptive behavior */ public logConnection( socket: plugins.net.Socket | plugins.tls.TLSSocket, eventType: 'connect' | 'close' | 'error', session?: ISmtpSession, error?: Error ): void { const clientInfo = { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, secure: socket instanceof plugins.tls.TLSSocket, sessionId: session?.id, sessionState: session?.state }; if (eventType === 'connect') { this.trackConnection(); } switch (this.currentMode) { case LogMode.VERBOSE: // Full connection logging switch (eventType) { case 'connect': this.log('info', `New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); break; case 'close': this.log('info', `Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); break; case 'error': this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { ...clientInfo, error }); break; } break; case LogMode.REDUCED: // Aggregate normal connections, log errors if (eventType === 'error') { this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { ...clientInfo, error }); } else { this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); } break; case LogMode.MINIMAL: // Only aggregate, except for critical errors if (eventType === 'error' && error && (error.message.includes('security') || error.message.includes('critical'))) { this.log('error', `Critical connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { ...clientInfo, error }); } else { this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); } break; } } /** * Log security event (always logged regardless of mode) */ public logSecurityEvent( level: SecurityLogLevel, type: SecurityEventType, message: string, details: Record, ipAddress?: string, domain?: string, success?: boolean ): void { const logLevel: LogLevel = level === SecurityLogLevel.DEBUG ? 'debug' : level === SecurityLogLevel.INFO ? 'info' : level === SecurityLogLevel.WARN ? 'warn' : 'error'; // Security events are always logged in full detail this.log(logLevel, message, { component: 'smtp-security', eventType: type, success, ipAddress, domain, ...details }); } /** * Determine appropriate log mode based on connection count */ private determineLogMode(activeConnections: number): LogMode { if (activeConnections >= this.config.reducedThreshold) { return LogMode.MINIMAL; } else if (activeConnections >= this.config.verboseThreshold) { return LogMode.REDUCED; } else { return LogMode.VERBOSE; } } /** * Switch to a new log mode */ private switchLogMode(newMode: LogMode): void { const oldMode = this.currentMode; this.currentMode = newMode; // Log the mode switch console.log(`[SMTP] Adaptive logging switched from ${oldMode} to ${newMode} (${this.connectionTracker.activeConnections} active connections)`); this.log('info', `Adaptive logging mode changed to ${newMode}`, { oldMode, newMode, activeConnections: this.connectionTracker.activeConnections, peakConnections: this.connectionTracker.peakConnections, totalConnections: this.connectionTracker.totalConnections }); // If switching to more verbose mode, flush aggregated entries if ((oldMode === LogMode.MINIMAL && newMode !== LogMode.MINIMAL) || (oldMode === LogMode.REDUCED && newMode === LogMode.VERBOSE)) { this.flushAggregatedEntries(); } } /** * Add entry to aggregation buffer */ private aggregateEntry( type: 'connection' | 'command' | 'response' | 'error', level: LogLevel, message: string, options?: ISmtpLogOptions ): void { const key = `${type}:${message}`; const now = Date.now(); if (this.aggregatedEntries.has(key)) { const entry = this.aggregatedEntries.get(key)!; entry.count++; entry.lastSeen = now; } else { this.aggregatedEntries.set(key, { type, count: 1, firstSeen: now, lastSeen: now, sample: { message, level, options } }); } // Force flush if we have too many entries if (this.aggregatedEntries.size >= this.config.maxAggregatedEntries) { this.flushAggregatedEntries(); } } /** * Start the aggregation timer */ private startAggregationTimer(): void { if (this.aggregationTimer) { clearInterval(this.aggregationTimer); } this.aggregationTimer = setInterval(() => { this.flushAggregatedEntries(); }, this.config.aggregationInterval); // Unref the timer so it doesn't keep the process alive if (this.aggregationTimer && typeof this.aggregationTimer.unref === 'function') { this.aggregationTimer.unref(); } } /** * Flush aggregated entries to logs */ private flushAggregatedEntries(): void { if (this.aggregatedEntries.size === 0) { return; } const summary: Record = {}; let totalAggregated = 0; for (const [key, entry] of this.aggregatedEntries.entries()) { summary[entry.type] = (summary[entry.type] || 0) + entry.count; totalAggregated += entry.count; // Log a sample of high-frequency entries if (entry.count >= 10) { this.log(entry.sample.level, `${entry.sample.message} (aggregated: ${entry.count} occurrences)`, { ...entry.sample.options, aggregated: true, occurrences: entry.count, timeSpan: entry.lastSeen - entry.firstSeen }); } } // Log aggregation summary console.log(`[SMTP] Aggregated ${totalAggregated} log entries: ${JSON.stringify(summary)}`); this.log('info', 'Aggregated log summary', { totalEntries: totalAggregated, breakdown: summary, logMode: this.currentMode, activeConnections: this.connectionTracker.activeConnections }); // Clear aggregated entries this.aggregatedEntries.clear(); } /** * Cleanup resources */ public destroy(): void { if (this.aggregationTimer) { clearInterval(this.aggregationTimer); this.aggregationTimer = null; } this.flushAggregatedEntries(); } } /** * Default instance for easy access */ export const adaptiveLogger = AdaptiveSmtpLogger.getInstance();