UNPKG

server-log-monitor

Version:

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

595 lines (505 loc) 21.1 kB
const axios = require('axios'); class ElevenLabsService { constructor(config = {}) { this.config = config; this.apiKey = config.apiKey || process.env.ELEVENLABS_API_KEY; this.agentId = config.agentId || process.env.ELEVENLABS_AGENT_ID; this.phoneNumberId = config.phoneNumberId || process.env.ELEVENLABS_PHONE_NUMBER_ID; this.baseURL = 'https://api.elevenlabs.io/v1'; if (!this.apiKey) { console.warn('⚠️ ElevenLabs API key not provided - conversational agent disabled'); this.enabled = false; return; } this.enabled = true; this.axiosInstance = axios.create({ baseURL: this.baseURL, headers: { 'xi-api-key': this.apiKey, 'Content-Type': 'application/json' }, timeout: 30000 }); } async createAgent(agentConfig = {}) { if (!this.enabled) { throw new Error('ElevenLabs service not enabled - API key required'); } try { console.log('🤖 Creating ElevenLabs conversational agent...'); const agentPrompt = agentConfig.prompt || this._getSystemPrompt(); const response = await this.axiosInstance.post('/convai/agents/create', { name: agentConfig.name || 'Server Log Monitoring Agent', conversation_config: { agent_prompt: agentPrompt, language: agentConfig.language || 'en', voice_id: agentConfig.voiceId || process.env.ELEVENLABS_VOICE_ID, response_strategy: 'balanced', llm_model: 'gpt-4o-mini' }, platform_settings: { widget_color: '#FF4444', conversation_starters: [ "What's the current server status?", "Explain the latest alert", "What should I do about this error?", "Is this something urgent?" ] } }); console.log(`✅ Agent created successfully with ID: ${response.data.agent_id}`); this.agentId = response.data.agent_id; return response.data; } catch (error) { console.error('❌ Failed to create ElevenLabs agent:', error.response?.data || error.message); throw error; } } async importTwilioPhoneNumber(twilioConfig) { try { console.log('📞 Importing Twilio phone number to ElevenLabs...'); const response = await this.axiosInstance.post('/convai/phone-numbers/import', { label: twilioConfig.label || 'Server Monitor Phone', phone_number: twilioConfig.phoneNumber, twilio_account_sid: twilioConfig.accountSid, twilio_auth_token: twilioConfig.authToken }); console.log(`✅ Phone number imported successfully with ID: ${response.data.phone_number_id}`); this.phoneNumberId = response.data.phone_number_id; return response.data; } catch (error) { console.error('❌ Failed to import Twilio phone number:', error.response?.data || error.message); throw error; } } async assignAgentToPhoneNumber() { try { if (!this.agentId || !this.phoneNumberId) { throw new Error('Agent ID and Phone Number ID are required'); } console.log('🔗 Assigning agent to phone number...'); const response = await this.axiosInstance.post(`/convai/phone-numbers/${this.phoneNumberId}/assign-agent`, { agent_id: this.agentId }); console.log('✅ Agent successfully assigned to phone number'); return response.data; } catch (error) { console.error('❌ Failed to assign agent to phone number:', error.response?.data || error.message); throw error; } } async updateAgentSystemPrompt() { if (!this.enabled) { console.log('⚠️ ElevenLabs service not enabled - skipping prompt update'); return null; } try { if (!this.agentId) { throw new Error('Agent ID is required to update system prompt'); } console.log('📝 Updating agent with system prompt and dynamic variables...'); const systemPrompt = this._getSystemPrompt(); const response = await this.axiosInstance.patch(`/convai/agents/${this.agentId}`, { conversation_config: { agent: { prompt: { prompt: systemPrompt, llm: 'gpt-4o-mini', temperature: 0.1, max_tokens: -1, dynamic_variables: { dynamic_variable_placeholders: { alert_severity: "Current alert severity level", alert_summary: "Analysis of the current alert", alert_file: "Log file where the alert was detected", alert_timestamp: "When the alert was triggered", error_timestamp: "When the actual error occurred in the logs", error_count: "Number of errors detected", error_details: "Specific error messages and severities", recent_logs: "Latest log entries for analysis", historical_logs: "Log entries from before the error occurred", timeline_analysis: "Sequence of events leading to the issue", system_status: "Current system health status" } } } } } }); console.log('✅ Agent system prompt and dynamic variables updated successfully'); return response.data; } catch (error) { console.error('❌ Failed to update agent system prompt:', error.response?.data || error.message); throw error; } } async updateAgentContext(context) { if (!this.enabled) { console.log('⚠️ ElevenLabs service not enabled - skipping context update'); return null; } try { if (!this.agentId) { throw new Error('Agent ID is required to update context'); } console.log('📋 Updating agent context with current log data...'); // Format context for ElevenLabs agent consumption const formattedContext = this._formatContextForAgent(context); const response = await this.axiosInstance.patch(`/convai/agents/${this.agentId}`, { conversation_config: { context: formattedContext } }); console.log('✅ Agent context updated successfully'); return response.data; } catch (error) { console.error('❌ Failed to update agent context:', error.response?.data || error.message); console.log('📋 Context that failed to update:', this._formatContextForAgent(context).substring(0, 200) + '...'); throw error; } } async makeOutboundCall(phoneNumber, context) { if (!this.enabled) { return { success: false, to: phoneNumber, error: 'ElevenLabs service not enabled', timestamp: new Date().toISOString() }; } try { if (!this.agentId || !this.phoneNumberId) { throw new Error('Agent ID and Phone Number ID are required for outbound calls'); } console.log(`📞 Initiating conversational outbound call to ${phoneNumber}...`); // Prepare dynamic variables for log context const dynamicVariables = this._prepareDynamicVariables(context); const response = await this.axiosInstance.post('/convai/twilio/outbound-call', { agent_id: this.agentId, agent_phone_number_id: this.phoneNumberId, to_number: phoneNumber, conversation_initiation_client_data: { dynamic_variables: dynamicVariables } }); console.log(`✅ Conversational call initiated successfully. Call SID: ${response.data.callSid}`); return { success: true, callId: response.data.callSid, conversationId: response.data.conversation_id, to: phoneNumber, timestamp: new Date().toISOString(), type: 'conversational' }; } catch (error) { console.error(`❌ Failed to make conversational call to ${phoneNumber}:`, error.response?.data || error.message); return { success: false, to: phoneNumber, error: error.response?.data?.message || error.message, timestamp: new Date().toISOString(), type: 'conversational' }; } } async getCallStatus(callId) { try { const response = await this.axiosInstance.get(`/convai/calls/${callId}`); return response.data; } catch (error) { console.error(`❌ Failed to get call status for ${callId}:`, error.response?.data || error.message); throw error; } } async testConnection() { try { const response = await this.axiosInstance.get('/user'); return { status: 'connected', message: 'ElevenLabs API connection successful', user: response.data, agentId: this.agentId, phoneNumberId: this.phoneNumberId }; } catch (error) { return { status: 'error', message: error.response?.data?.message || error.message, agentId: this.agentId, phoneNumberId: this.phoneNumberId }; } } _prepareDynamicVariables(context) { // Prepare dynamic variables for ElevenLabs agent const alert = context.alert_trigger || context; return { alert_severity: alert.severity || alert.status || 'unknown', alert_summary: alert.summary || alert.analysis || 'No analysis available', alert_file: alert.file_path || 'Unknown file', alert_timestamp: context.timestamp || new Date().toISOString(), error_timestamp: this._extractErrorTimestamp(alert.errors) || context.timestamp || new Date().toISOString(), error_count: (alert.errors && alert.errors.length) || 0, error_details: this._formatErrorDetails(alert.errors), recent_logs: context.recent_logs || 'No recent logs available', historical_logs: this._formatHistoricalLogs(context.historical_logs), timeline_analysis: this._generateTimelineAnalysis(context), system_status: context.system_status || 'Status unknown' }; } _formatErrorDetails(errors) { if (!errors || !Array.isArray(errors) || errors.length === 0) { return 'No errors detected'; } return errors.slice(0, 3).map((error, index) => { const timestamp = this._extractTimestampFromError(error); const timeInfo = timestamp ? ` (occurred at ${timestamp})` : ''; return `${index + 1}. ${error.severity || 'ERROR'}: ${error.message || 'Unknown error'}${timeInfo}`; }).join('\n'); } _extractErrorTimestamp(errors) { if (!errors || !Array.isArray(errors) || errors.length === 0) { return null; } // Extract timestamp from first error's log content const firstError = errors[0]; return this._extractTimestampFromError(firstError); } _extractTimestampFromError(error) { if (!error) return null; // Debug log to see error structure // console.log('🔍 Debugging error structure for timestamp extraction:', { // keys: Object.keys(error), // hasLine: !!error.line, // hasMessage: !!error.message, // hasLogLines: !!error.logLines, // camelCase // hasLogLinesSnake: !!error.log_lines, // snake_case // hasLines: !!error.lines, // logLinesLength: error.logLines ? error.logLines.length : 0, // firstLogLinePreview: error.logLines && error.logLines[0] ? error.logLines[0].substring(0, 100) : null, // messagePreview: error.message ? error.message.substring(0, 100) : null // }); // Try to extract timestamp from various possible log line fields const possibleLogFields = [ error.line, error.message, error.log_line, error.first_line, error.example_line ]; for (const logContent of possibleLogFields) { if (logContent && typeof logContent === 'string') { const extractedTimestamp = this._parseTimestampFromLogLine(logContent); if (extractedTimestamp) { console.log('✅ Found timestamp in log content:', extractedTimestamp); return extractedTimestamp; } } } // Check in various log_lines arrays const possibleLogArrays = [ error.logLines, // camelCase - this is the correct one! error.log_lines, // snake_case error.lines, error.examples, error.log_entries ]; for (const logArray of possibleLogArrays) { if (logArray && Array.isArray(logArray) && logArray.length > 0) { const firstLogLine = logArray[0]; const extractedTimestamp = this._parseTimestampFromLogLine(firstLogLine); if (extractedTimestamp) { console.log('✅ Found timestamp in log array:', extractedTimestamp); return extractedTimestamp; } } } console.log('❌ No timestamp found in log content, using fallback'); // Fallback to error metadata timestamp if available return error.timestamp || error.time || null; } _parseTimestampFromLogLine(logLine) { if (!logLine) return null; // Pattern for Apache/Nginx access log 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 = logLine.match(apachePattern); if (apacheMatch) { try { // Convert "18 Sept 2025, 18:10:26" to ISO format const dateStr = apacheMatch[1].replace(',', ''); const date = new Date(dateStr); return date.toISOString(); } 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 = logLine.match(standardPattern); if (standardMatch) { try { return new Date(standardMatch[1]).toISOString(); } catch (e) { // Continue to next pattern } } // Pattern for ISO timestamps: 2025-09-18T18:10:26Z const isoPattern = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z?)/; const isoMatch = logLine.match(isoPattern); if (isoMatch) { try { return new Date(isoMatch[1]).toISOString(); } catch (e) { // Continue to next pattern } } return null; } _formatHistoricalLogs(historicalLogs) { if (!historicalLogs || typeof historicalLogs !== 'object') { return 'No historical log data available'; } // Check if it's the new dictionary format if (historicalLogs.error_1 || historicalLogs.error_2 || historicalLogs.error_3) { const formattedSections = []; Object.keys(historicalLogs).forEach(errorKey => { const errorData = historicalLogs[errorKey]; formattedSections.push(`\n${errorKey.toUpperCase()} (${errorData.error_description}):`); formattedSections.push(`Error occurred at: ${errorData.error_timestamp}`); formattedSections.push(`Logs before error (${errorData.count} entries):`); if (errorData.logs_before && errorData.logs_before.length > 0) { errorData.logs_before.slice(-5).forEach(log => { const timestamp = log.timestamp ? (log.timestamp instanceof Date ? log.timestamp.toISOString() : log.timestamp) : 'Unknown time'; formattedSections.push(` ${timestamp}: ${log.content || 'No content'}`); }); } else { formattedSections.push(' No historical logs found before this error'); } }); return formattedSections.join('\n'); } // Fallback for old array format if (Array.isArray(historicalLogs)) { return historicalLogs.slice(-5).map(log => { const timestamp = log.timestamp ? (log.timestamp instanceof Date ? log.timestamp.toISOString() : log.timestamp) : 'Unknown time'; return `${timestamp}: ${log.message || log.content || 'No message'}`; }).join('\n'); } return 'No historical log data available'; } _generateTimelineAnalysis(context) { const alert = context.alert_trigger || context; const events = []; if (context.historical_logs && context.historical_logs.length > 0) { events.push('Historical activity detected in logs'); } if (alert.errors && alert.errors.length > 0) { events.push(`${alert.errors.length} error(s) occurred`); } if (context.timestamp) { events.push(`Alert triggered at ${context.timestamp}`); } return events.length > 0 ? events.join(' → ') : 'No timeline data available'; } _formatContextForAgent(context) { // Simplified context formatting focused on log data const alert = context.alert_trigger || context; let contextString = `CURRENT SERVER ALERT:\n`; contextString += `Severity: ${alert.severity || alert.status || 'unknown'}\n`; contextString += `Summary: ${alert.summary || alert.analysis || 'No analysis available'}\n`; contextString += `File: ${alert.file_path || 'Unknown file'}\n`; contextString += `Timestamp: ${context.timestamp || new Date().toISOString()}\n\n`; // Add error details if available if (alert.errors && Array.isArray(alert.errors)) { contextString += `DETECTED ERRORS (${alert.errors.length}):\n`; alert.errors.slice(0, 5).forEach((error, index) => { contextString += `${index + 1}. ${error.severity || 'ERROR'}: ${error.message || 'Unknown error'}\n`; if (error.line) { contextString += ` Log Line: ${error.line}\n`; } }); contextString += `\n`; } // Add recent log context if available if (context.recent_logs) { contextString += `RECENT LOG ACTIVITY:\n`; contextString += context.recent_logs + '\n\n'; } // Add system status if available if (context.system_status) { contextString += `SYSTEM STATUS:\n`; contextString += `${context.system_status}\n\n`; } contextString += `You are helping the user understand this server alert. `; contextString += `Explain what's happening, assess the severity, and provide guidance on next steps. `; contextString += `Be clear, technical but accessible, and focus on actionable advice.`; return contextString; } getConfiguration() { return { elevenlabs: { enabled: this.enabled, configured: !!this.apiKey, agentId: this.agentId || 'Not set', phoneNumberId: this.phoneNumberId || 'Not set', baseURL: this.baseURL } }; } // Simple method to check if service is ready isReady() { return this.enabled && !!this.agentId && !!this.phoneNumberId; } _getSystemPrompt() { const fs = require('fs'); const path = require('path'); try { const promptPath = path.join(__dirname, '..', '..', 'prompts', 'elevenlabs-agent-system-prompt.md'); if (fs.existsSync(promptPath)) { return fs.readFileSync(promptPath, 'utf8'); } } catch (error) { console.warn('Could not load system prompt file, using fallback'); } return 'You are an advanced server monitoring assistant. Provide clear, technical guidance for server issues.'; } // Method to simulate a conversational response (for testing) async simulateConversation(logContext, userQuestion = "What's happening with my server?") { if (!this.enabled) { return { response: "I'm sorry, but the conversational agent is not configured. Please check your ElevenLabs API settings.", context: logContext }; } // Simulate agent response based on log context const context = this._formatContextForAgent(logContext); const alert = logContext.alert_trigger || logContext; let response = ""; if (alert.severity === 'critical' || alert.status === 'critical') { response = `I see a critical alert in your system. ${alert.analysis || alert.summary || 'There are serious issues that need immediate attention.'}. `; response += `This requires urgent action. `; } else if (alert.severity === 'warning' || alert.status === 'warning') { response = `There's a warning-level issue detected. ${alert.analysis || alert.summary || 'The system is experiencing some problems.'}. `; response += `You should investigate this soon. `; } else { response = `I'm monitoring your server logs. ${alert.analysis || alert.summary || 'The system appears to be running normally.'}. `; } if (alert.errors && alert.errors.length > 0) { response += `I found ${alert.errors.length} error${alert.errors.length > 1 ? 's' : ''} that need attention. `; response += `The main issue appears to be: ${alert.errors[0].message || 'an unspecified error'}. `; } response += userQuestion ? `Regarding your question "${userQuestion}": ` : ''; response += "Would you like me to explain any specific part of this alert or help you troubleshoot?"; return { response, context, severity: alert.severity || alert.status, timestamp: new Date().toISOString() }; } } module.exports = ElevenLabsService;