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