UNPKG

server-log-monitor

Version:

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

430 lines (357 loc) 14 kB
const fs = require('fs-extra'); const path = require('path'); class LogContextService { constructor(config = {}) { this.config = config; this.projectRoot = config.projectRoot || process.cwd(); this.maxLogLines = config.maxLogLines || 100; this.maxContextLength = config.maxContextLength || 2000; } async prepareAgentContext(analysis, filePath) { try { console.log('📋 Preparing agent context for conversational call...'); const context = { alert_trigger: { severity: analysis.status || 'unknown', summary: analysis.analysis || 'No analysis available', file_path: filePath || 'Unknown file', timestamp: new Date().toISOString(), errors: analysis.errors || [] }, recent_logs: await this._getRecentLogContext(filePath), historical_logs: await this._getHistoricalLogContext(filePath, analysis.errors), timeline_analysis: this._generateTimelineAnalysis(analysis.errors), system_status: await this._getSystemStatus(), conversation_guidance: this._getConversationGuidance(analysis) }; console.log(`✅ Context prepared with ${context.alert_trigger.errors.length} errors and recent log data`); return context; } catch (error) { console.error('❌ Failed to prepare agent context:', error.message); return this._getFallbackContext(analysis, filePath); } } async _getRecentLogContext(filePath) { try { if (!filePath || !await fs.pathExists(filePath)) { return 'No recent log data available'; } const content = await fs.readFile(filePath, 'utf8'); const lines = content.split('\n').filter(line => line.trim()); // Get last N lines const recentLines = lines.slice(-this.maxLogLines); // Truncate if too long for context let logContext = recentLines.join('\n'); if (logContext.length > this.maxContextLength) { logContext = logContext.substring(logContext.length - this.maxContextLength); logContext = '...' + logContext.substring(logContext.indexOf('\n') + 1); } return logContext; } catch (error) { console.error('Error reading log context:', error.message); return 'Unable to read recent log data'; } } async _getSystemStatus() { // Simple system status - can be enhanced with actual system checks const status = { timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), platform: process.platform }; return `System running for ${Math.floor(status.uptime / 3600)} hours. Memory usage: ${Math.round(status.memory.heapUsed / 1024 / 1024)}MB. Platform: ${status.platform}`; } _getConversationGuidance(analysis) { const severity = analysis.status || 'unknown'; const guidance = { critical: { tone: 'urgent but calm', focus: 'immediate action required', talking_points: [ 'Explain the critical nature of the alert', 'Identify what systems are affected', 'Provide immediate troubleshooting steps', 'Suggest when to escalate to team members' ] }, warning: { tone: 'informative and proactive', focus: 'preventive action recommended', talking_points: [ 'Explain what the warning indicates', 'Assess potential impact if ignored', 'Provide preventive measures', 'Suggest monitoring frequency' ] }, normal: { tone: 'reassuring and informative', focus: 'status update and guidance', talking_points: [ 'Confirm system is operating normally', 'Explain any minor issues detected', 'Provide general maintenance suggestions', 'Offer to answer specific questions' ] } }; return guidance[severity] || guidance.normal; } _getFallbackContext(analysis, filePath) { return { alert_trigger: { severity: analysis?.status || 'unknown', summary: analysis?.analysis || 'System alert detected but unable to load full context', file_path: filePath || 'Unknown file', timestamp: new Date().toISOString(), errors: analysis?.errors || [] }, recent_logs: 'Unable to load recent log data', historical_logs: [], timeline_analysis: 'No timeline data available', system_status: 'System status unavailable', conversation_guidance: this._getConversationGuidance(analysis || {}) }; } // Method to enhance context with additional log sources async enhanceContextWithAdditionalLogs(context, additionalLogPaths = []) { if (!additionalLogPaths.length) return context; try { const additionalLogs = {}; for (const logPath of additionalLogPaths) { try { if (await fs.pathExists(logPath)) { const content = await fs.readFile(logPath, 'utf8'); const lines = content.split('\n').filter(line => line.trim()); const recentLines = lines.slice(-20); // Smaller sample for additional logs const fileName = path.basename(logPath); additionalLogs[fileName] = recentLines.join('\n'); } } catch (error) { console.warn(`Could not read additional log: ${logPath}`); } } if (Object.keys(additionalLogs).length > 0) { context.additional_logs = additionalLogs; console.log(`✅ Enhanced context with ${Object.keys(additionalLogs).length} additional log sources`); } return context; } catch (error) { console.error('Error enhancing context with additional logs:', error.message); return context; } } // Method to create a summary for voice consumption createVoiceSummary(context) { const alert = context.alert_trigger; let summary = ``; // Severity announcement if (alert.severity === 'critical') { summary += `Critical alert detected. `; } else if (alert.severity === 'warning') { summary += `Warning level alert detected. `; } else { summary += `System alert detected. `; } // Main summary summary += alert.summary || 'System monitoring has detected an issue. '; // Error count if (alert.errors && alert.errors.length > 0) { summary += `There ${alert.errors.length === 1 ? 'is' : 'are'} ${alert.errors.length} error${alert.errors.length > 1 ? 's' : ''} detected. `; } // Guidance based on severity const guidance = context.conversation_guidance; if (guidance && guidance.focus) { summary += guidance.focus + '. '; } summary += `I can provide more details about the specific issues and help you troubleshoot.`; return summary; } // Method to format context for different output formats formatContextForOutput(context, format = 'agent') { switch (format) { case 'agent': return this._formatForAgent(context); case 'voice': return this.createVoiceSummary(context); case 'json': return JSON.stringify(context, null, 2); default: return context; } } _formatForAgent(context) { const alert = context.alert_trigger; let formatted = `CURRENT ALERT:\n`; formatted += `Severity: ${alert.severity}\n`; formatted += `Summary: ${alert.summary}\n`; formatted += `File: ${alert.file_path}\n`; formatted += `Time: ${alert.timestamp}\n\n`; if (alert.errors && alert.errors.length > 0) { formatted += `ERRORS DETECTED (${alert.errors.length}):\n`; alert.errors.slice(0, 5).forEach((error, i) => { formatted += `${i + 1}. ${error.severity || 'ERROR'}: ${error.message}\n`; }); formatted += '\n'; } if (context.recent_logs && context.recent_logs !== 'Unable to load recent log data') { formatted += `RECENT LOG ACTIVITY:\n`; formatted += context.recent_logs + '\n\n'; } formatted += `GUIDANCE: ${context.conversation_guidance?.focus || 'Provide helpful analysis'}\n`; return formatted; } // Utility method to validate context isValidContext(context) { return context && context.alert_trigger && context.alert_trigger.severity && context.alert_trigger.summary; } // Method to get context statistics getContextStats(context) { const alert = context.alert_trigger || {}; const stats = { severity: alert.severity || 'unknown', errorCount: (alert.errors || []).length, hasRecentLogs: !!(context.recent_logs && context.recent_logs !== 'Unable to load recent log data'), hasSystemStatus: !!(context.system_status && context.system_status !== 'System status unavailable'), contextSize: JSON.stringify(context).length, timestamp: alert.timestamp || new Date().toISOString() }; return stats; } async _getHistoricalLogContext(filePath, errors) { try { if (!filePath || !await fs.pathExists(filePath) || !errors || errors.length === 0) { return {}; } const content = await fs.readFile(filePath, 'utf8'); const lines = content.split('\n').filter(line => line.trim()); const historicalLogsPerError = {}; // Get historical logs for each error for (let i = 0; i < errors.length; i++) { const error = errors[i]; const errorTimestamp = this._getErrorTimestampFromLogLines(error); if (errorTimestamp) { console.log(`🔍 Finding historical logs before error ${i + 1}:`, errorTimestamp.toISOString()); // Find logs from before this specific error const historicalLines = this._getLogsBefore(lines, errorTimestamp); const historicalData = historicalLines.slice(-10).map(line => ({ content: line, timestamp: this._extractTimestampFromLogLine(line) })); historicalLogsPerError[`error_${i + 1}`] = { error_description: error.message || 'Unknown error', error_timestamp: errorTimestamp.toISOString(), logs_before: historicalData, count: historicalData.length }; console.log(`✅ Found ${historicalData.length} historical log entries before error ${i + 1}`); } } return historicalLogsPerError; } catch (error) { console.error('Error reading historical log context:', error.message); return {}; } } _findEarliestErrorTimestamp(errors) { if (!errors || !Array.isArray(errors)) return null; const timestamps = errors .map(error => error.timestamp || error.time) .filter(ts => ts) .sort(); return timestamps.length > 0 ? timestamps[0] : null; } _findEarliestErrorFromLogLines(errors) { if (!errors || !Array.isArray(errors)) return null; let earliestTime = null; for (const error of errors) { if (error.logLines && Array.isArray(error.logLines)) { for (const logLine of error.logLines) { const timestamp = this._extractTimestampFromLogLine(logLine); if (timestamp) { if (!earliestTime || timestamp < earliestTime) { earliestTime = timestamp; } } } } } return earliestTime; } _getErrorTimestampFromLogLines(error) { if (!error || !error.logLines || !Array.isArray(error.logLines) || error.logLines.length === 0) { return null; } // Get timestamp from the first log line of this error const firstLogLine = error.logLines[0]; return this._extractTimestampFromLogLine(firstLogLine); } _getLogsBefore(lines, targetTimestamp) { const beforeLines = []; const targetTime = targetTimestamp instanceof Date ? targetTimestamp : new Date(targetTimestamp); for (const line of lines) { // Try to extract timestamp from log line const logTime = this._extractTimestampFromLogLine(line); if (logTime && logTime < targetTime) { beforeLines.push(line); } else if (logTime && logTime >= targetTime) { // Found logs at or after target time, stop collecting break; } // Skip lines we can't parse timestamps for } return beforeLines; } _extractTimestampFromLogLine(line) { if (!line) return null; // Basic timestamp extraction for Apache/Nginx format: [18 Sept 2025, 18:10:26] const apachePattern = /\[(\d{1,2}\s+\w+\s+\d{4},\s+\d{2}:\d{2}:\d{2})\]/; const apacheMatch = line.match(apachePattern); if (apacheMatch) { try { // Convert "18 Sept 2025, 18:10:26" to ISO format const dateStr = apacheMatch[1].replace(',', ''); return new Date(dateStr); } catch (e) { // Continue to next pattern } } // Pattern for standard log timestamps: [2025-09-18 18:10:26] const standardPattern = /\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\]/; const standardMatch = line.match(standardPattern); if (standardMatch) { try { return new Date(standardMatch[1]); } catch (e) { // Continue to next pattern } } return null; } _generateTimelineAnalysis(errors) { if (!errors || !Array.isArray(errors) || errors.length === 0) { return 'No timeline data available'; } const events = []; // Group errors by type if possible const errorTypes = {}; errors.forEach(error => { const type = error.type || error.level || error.severity || 'ERROR'; errorTypes[type] = (errorTypes[type] || 0) + 1; }); // Build timeline description const sortedTypes = Object.entries(errorTypes).sort((a, b) => b[1] - a[1]); sortedTypes.forEach(([type, count]) => { events.push(`${count} ${type}${count > 1 ? 's' : ''} detected`); }); if (events.length === 0) { events.push('Multiple errors detected'); } return events.join(' → '); } } module.exports = LogContextService;