import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; import { logBuffer } from '../../logger.js'; export class LogsHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(private opsServerRef: OpsServer) { // Add this handler's router to the parent this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } private registerHandlers(): void { // Get Recent Logs Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getRecentLogs', async (dataArg, toolsArg) => { const logs = await this.getRecentLogs( dataArg.level, dataArg.category, dataArg.limit || 100, dataArg.offset || 0, dataArg.search, dataArg.timeRange ); return { logs, total: logs.length, // TODO: Implement proper total count hasMore: false, // TODO: Implement proper pagination }; } ) ); // Get Log Stream Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getLogStream', async (dataArg, toolsArg) => { // Create a virtual stream for log streaming const virtualStream = new plugins.typedrequest.VirtualStream(); // Set up log streaming const streamLogs = this.setupLogStream( virtualStream, dataArg.filters?.level, dataArg.filters?.category, dataArg.follow ); // Start streaming streamLogs.start(); // VirtualStream handles cleanup automatically return { logStream: virtualStream as any, // Cast to IVirtualStream interface }; } ) ); } private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' { switch (smartlogLevel) { case 'silly': case 'debug': return 'debug'; case 'warn': return 'warn'; case 'error': return 'error'; default: return 'info'; } } private static deriveCategory( zone?: string, message?: string ): 'smtp' | 'dns' | 'security' | 'system' | 'email' { const msg = (message || '').toLowerCase(); if (msg.includes('[security:') || msg.includes('security')) return 'security'; if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta')) return 'email'; if (zone === 'dns' || msg.includes('dns')) return 'dns'; if (msg.includes('smtp')) return 'smtp'; return 'system'; } private async getRecentLogs( level?: 'error' | 'warn' | 'info' | 'debug', category?: 'smtp' | 'dns' | 'security' | 'system' | 'email', limit: number = 100, offset: number = 0, search?: string, timeRange?: '1h' | '6h' | '24h' | '7d' | '30d' ): Promise> { // Compute a timestamp cutoff from timeRange let since: number | undefined; if (timeRange) { const rangeMs: Record = { '1h': 3600000, '6h': 21600000, '24h': 86400000, '7d': 604800000, '30d': 2592000000, }; since = Date.now() - (rangeMs[timeRange] || 86400000); } // Map the UI level to smartlog levels for filtering const smartlogLevels: string[] | undefined = level ? level === 'debug' ? ['debug', 'silly'] : level === 'info' ? ['info', 'ok', 'success', 'note', 'lifecycle'] : [level] : undefined; // Fetch a larger batch from buffer, then apply category filter client-side const rawEntries = logBuffer.getEntries({ level: smartlogLevels as any, search, since, limit: limit * 3, // over-fetch to compensate for category filtering offset: 0, }); // Map ILogPackage → UI log format and apply category filter const mapped: Array<{ timestamp: number; level: 'debug' | 'info' | 'warn' | 'error'; category: 'smtp' | 'dns' | 'security' | 'system' | 'email'; message: string; metadata?: any; }> = []; for (const pkg of rawEntries) { const uiLevel = LogsHandler.mapLogLevel(pkg.level); const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message); if (category && uiCategory !== category) continue; mapped.push({ timestamp: pkg.timestamp, level: uiLevel, category: uiCategory, message: pkg.message, metadata: pkg.data, }); if (mapped.length >= limit) break; } return mapped; } private setupLogStream( virtualStream: plugins.typedrequest.VirtualStream, levelFilter?: string[], categoryFilter?: string[], follow: boolean = true ): { start: () => void; stop: () => void; } { let intervalId: NodeJS.Timeout | null = null; let logIndex = 0; const start = () => { if (!follow) { // Send existing logs and close this.getRecentLogs( levelFilter?.[0] as any, categoryFilter?.[0] as any, 100, 0 ).then(logs => { logs.forEach(log => { const logData = JSON.stringify(log); const encoder = new TextEncoder(); virtualStream.sendData(encoder.encode(logData)); }); // VirtualStream doesn't have end() method - it closes automatically }); return; } // For follow mode, simulate real-time log streaming intervalId = setInterval(async () => { const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; const mockCategory = categories[Math.floor(Math.random() * categories.length)]; const mockLevel = levels[Math.floor(Math.random() * levels.length)]; // Filter by requested criteria if (levelFilter && !levelFilter.includes(mockLevel)) return; if (categoryFilter && !categoryFilter.includes(mockCategory)) return; const logEntry = { timestamp: Date.now(), level: mockLevel, category: mockCategory, message: `Real-time log ${logIndex++} from ${mockCategory}`, metadata: { requestId: plugins.uuid.v4(), }, }; const logData = JSON.stringify(logEntry); const encoder = new TextEncoder(); try { await virtualStream.sendData(encoder.encode(logData)); } catch { // Stream closed or errored — clean up to prevent interval leak clearInterval(intervalId!); intervalId = null; } }, 2000); // Send a log every 2 seconds // TODO: Hook into actual logger events // logger.on('log', (logEntry) => { // if (matchesCriteria(logEntry, level, service)) { // virtualStream.sendData(formatLogEntry(logEntry)); // } // }); }; const stop = () => { if (intervalId) { clearInterval(intervalId); intervalId = null; } // TODO: Unhook from logger events }; return { start, stop }; } }