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
JavaScript
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;