UNPKG

server-log-monitor

Version:

AI-powered server log monitoring with BlackBox AI analysis, ElevenLabs conversational agents, file source detection, and intelligent voice alerts

311 lines (260 loc) 8.84 kB
const fs = require('fs').promises; const path = require('path'); const { EventEmitter } = require('events'); const { isBinaryFile } = require('isbinaryfile'); class LogMonitor extends EventEmitter { constructor(config) { super(); this.config = config; this.logPaths = config.logMonitor.paths || ['/var/log']; this.patterns = config.logMonitor.patterns || ['*.log', '*.txt']; this.maxLines = config.logMonitor.maxLines || 100; this.encoding = config.logMonitor.encoding || 'utf8'; this.pollInterval = config.logMonitor.pollInterval || 5000; this.pollingTimer = null; this.fileStates = new Map(); this.isMonitoring = false; } async startMonitoring() { if (this.isMonitoring) { console.warn('LogMonitor is already running'); return; } console.log('Starting log monitoring with polling...'); this.isMonitoring = true; try { await this._validatePaths(); await this._initializeFileStates(); this._startPolling(); this.emit('started'); console.log(`Monitoring ${this.fileStates.size} log files with ${this.pollInterval}ms polling interval`); } catch (error) { this.isMonitoring = false; console.error('Failed to start monitoring:', error.message); this.emit('error', error); throw error; } } async stopMonitoring() { if (!this.isMonitoring) { return; } console.log('Stopping log monitoring...'); this.isMonitoring = false; if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; } this.emit('stopped'); console.log('Log monitoring stopped'); } _startPolling() { console.log(`🔄 Starting polling every ${this.pollInterval}ms for ${this.maxLines} lines`); this.pollingTimer = setInterval(async () => { if (!this.isMonitoring) return; try { await this._pollAllFiles(); } catch (error) { console.error('Error during polling:', error.message); this.emit('error', error); } }, this.pollInterval); // Run initial poll immediately (force read all files) console.log('🚀 Running initial poll immediately...'); this._pollAllFiles(true).catch(error => { console.error('Error during initial poll:', error.message); }); } async _pollAllFiles(forceRead = false) { const filesToPoll = Array.from(this.fileStates.keys()); for (const filePath of filesToPoll) { try { await this._pollFile(filePath, forceRead); } catch (error) { console.warn(`Failed to poll ${filePath}: ${error.message}`); } } } async _pollFile(filePath, forceRead = false) { try { const stat = await fs.stat(filePath); const fileState = this.fileStates.get(filePath); if (!fileState) { // File was removed from monitoring return; } // Check if file has been modified since last poll (skip check if forcing read) if (!forceRead && stat.mtime <= fileState.lastModified && stat.size === fileState.size) { // No changes return; } // Check if file is binary before reading const isBinary = await this._isBinaryFile(filePath); if (isBinary) { console.log(`⚠️ Skipping binary file: ${filePath}`); return; } // Read the last N lines from the file const content = await this._readLastLines(filePath, this.maxLines); if (content && content.trim()) { // Update file state fileState.lastModified = stat.mtime; fileState.size = stat.size; fileState.lastProcessed = new Date(); // Emit new content event this.emit('newContent', { filePath, content, timestamp: new Date(), pollInterval: this.pollInterval, maxLines: this.maxLines }); } } catch (error) { if (error.code === 'ENOENT') { // File was deleted this.fileStates.delete(filePath); this.emit('fileRemoved', { filePath }); } else { throw error; } } } async _validatePaths() { const validPaths = []; for (const logPath of this.logPaths) { try { const stat = await fs.stat(logPath); if (stat.isDirectory() || stat.isFile()) { validPaths.push(logPath); } } catch (error) { console.warn(`Log path not accessible: ${logPath} - ${error.message}`); } } if (validPaths.length === 0) { throw new Error('No valid log paths found'); } this.logPaths = validPaths; } async _initializeFileStates() { const logFiles = await this._discoverLogFiles(); for (const filePath of logFiles) { try { const stat = await fs.stat(filePath); this.fileStates.set(filePath, { size: stat.size, lastModified: stat.mtime, lastProcessed: new Date() }); } catch (error) { console.warn(`Could not initialize state for ${filePath}:`, error.message); } } } async _discoverLogFiles() { const logFiles = new Set(); for (const basePath of this.logPaths) { try { const stat = await fs.stat(basePath); if (stat.isFile()) { logFiles.add(basePath); } else if (stat.isDirectory()) { const files = await this._findLogFilesInDirectory(basePath); files.forEach(file => logFiles.add(file)); } } catch (error) { console.warn(`Error processing path ${basePath}:`, error.message); } } return Array.from(logFiles); } async _findLogFilesInDirectory(dirPath) { const logFiles = []; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isFile() && this._matchesPattern(entry.name)) { logFiles.push(fullPath); } else if (entry.isDirectory() && this.config.logMonitor.recursive) { const subFiles = await this._findLogFilesInDirectory(fullPath); logFiles.push(...subFiles); } } } catch (error) { console.warn(`Error reading directory ${dirPath}:`, error.message); } return logFiles; } _matchesPattern(filename) { return this.patterns.some(pattern => { const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.')); return regex.test(filename); }); } async _isBinaryFile(filePath) { try { // First check file extension const binaryExtensions = [ '.exe', '.bin', '.dll', '.so', '.dylib', '.app', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.tiff', '.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.zip', '.rar', '.tar', '.gz', '.7z', '.bz2', '.db', '.sqlite', '.mdb', '.accdb', '.class', '.jar', '.war', '.ear' ]; const ext = path.extname(filePath).toLowerCase(); if (binaryExtensions.includes(ext)) { return true; } // Then check file content using isbinaryfile return await isBinaryFile(filePath); } catch (error) { console.warn(`Could not check if file is binary: ${filePath}`, error.message); return false; // Default to text if we can't determine } } async _readLastLines(filePath, lineCount) { try { const content = await fs.readFile(filePath, this.encoding); const lines = content.split('\n').filter(line => line.trim()); return lines.slice(-lineCount).join('\n'); } catch (error) { throw new Error(`Failed to read last lines from ${filePath}: ${error.message}`); } } async getMonitoringStatus() { // Initialize file states if not already done if (this.fileStates.size === 0) { try { await this._validatePaths(); await this._initializeFileStates(); } catch (error) { console.warn('Could not initialize file states:', error.message); } } return { isMonitoring: this.isMonitoring, watchedPaths: this.logPaths, monitoredFiles: Array.from(this.fileStates.keys()), fileCount: this.fileStates.size, config: { maxLines: this.maxLines, patterns: this.patterns, pollInterval: this.pollInterval }, monitoringMode: 'polling' }; } async readLogSnapshot(filePath, lines = null) { const linesToRead = lines || this.maxLines; try { return await this._readLastLines(filePath, linesToRead); } catch (error) { throw new Error(`Failed to read snapshot from ${filePath}: ${error.message}`); } } } module.exports = LogMonitor;