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