Skip to main content
Glama
ooples

MCP Console Automation Server

AlertManager.ts18.4 kB
import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import * as nodemailer from 'nodemailer'; import axios from 'axios'; import { Alert, NotificationConfig, NotificationTrigger, Anomaly, } from '../types/index.js'; import { Logger } from '../utils/logger.js'; interface AlertRule { id: string; name: string; description: string; enabled: boolean; conditions: AlertCondition[]; notifications: NotificationConfig[]; cooldownMinutes: number; lastTriggered?: Date; } interface AlertCondition { metric: string; operator: 'gt' | 'lt' | 'eq' | 'ne' | 'gte' | 'lte'; threshold: number; duration: number; // minutes } interface NotificationChannel { type: 'email' | 'webhook' | 'slack' | 'console'; config: Record<string, any>; enabled: boolean; } export class AlertManager extends EventEmitter { private logger: Logger; private alerts: Map<string, Alert> = new Map(); private alertRules: Map<string, AlertRule> = new Map(); private notificationChannels: Map<string, NotificationChannel> = new Map(); private metricValues: Map<string, { value: number; timestamp: Date }[]> = new Map(); private isRunning: boolean = false; private evaluationInterval: NodeJS.Timeout | null = null; constructor() { super(); this.logger = new Logger('AlertManager'); this.setupDefaultRules(); } start(): void { if (this.isRunning) { return; } this.isRunning = true; this.logger.info('Starting alert manager'); // Evaluate rules every minute this.evaluationInterval = setInterval(() => { this.evaluateRules(); }, 60000); } stop(): void { if (!this.isRunning) { return; } this.isRunning = false; if (this.evaluationInterval) { clearInterval(this.evaluationInterval); this.evaluationInterval = null; } this.logger.info('Stopped alert manager'); } // Setup default alert rules private setupDefaultRules(): void { // High CPU usage alert this.addAlertRule({ id: 'high-cpu-usage', name: 'High CPU Usage', description: 'Alert when CPU usage exceeds 90% for more than 5 minutes', enabled: true, conditions: [ { metric: 'cpu_usage', operator: 'gt', threshold: 90, duration: 5, }, ], notifications: [], cooldownMinutes: 15, }); // High memory usage alert this.addAlertRule({ id: 'high-memory-usage', name: 'High Memory Usage', description: 'Alert when memory usage exceeds 95% for more than 3 minutes', enabled: true, conditions: [ { metric: 'memory_usage', operator: 'gt', threshold: 95, duration: 3, }, ], notifications: [], cooldownMinutes: 10, }); // Session failure alert this.addAlertRule({ id: 'session-failure-rate', name: 'High Session Failure Rate', description: 'Alert when session failure rate exceeds 10% in 10 minutes', enabled: true, conditions: [ { metric: 'session_failure_rate', operator: 'gt', threshold: 10, duration: 10, }, ], notifications: [], cooldownMinutes: 30, }); // Disk space alert this.addAlertRule({ id: 'low-disk-space', name: 'Low Disk Space', description: 'Alert when disk usage exceeds 95%', enabled: true, conditions: [ { metric: 'disk_usage', operator: 'gt', threshold: 95, duration: 1, }, ], notifications: [], cooldownMinutes: 60, }); } // Add a new alert rule addAlertRule(rule: AlertRule): void { this.alertRules.set(rule.id, rule); this.logger.info(`Added alert rule: ${rule.name}`); } // Update an existing alert rule updateAlertRule(ruleId: string, updates: Partial<AlertRule>): void { const rule = this.alertRules.get(ruleId); if (!rule) { throw new Error(`Alert rule not found: ${ruleId}`); } const updatedRule = { ...rule, ...updates }; this.alertRules.set(ruleId, updatedRule); this.logger.info(`Updated alert rule: ${rule.name}`); } // Remove an alert rule removeAlertRule(ruleId: string): void { const rule = this.alertRules.get(ruleId); if (rule) { this.alertRules.delete(ruleId); this.logger.info(`Removed alert rule: ${rule.name}`); } } // Add notification channel addNotificationChannel( channelId: string, channel: NotificationChannel ): void { this.notificationChannels.set(channelId, channel); this.logger.info(`Added notification channel: ${channel.type}`); } // Update metric value for rule evaluation updateMetricValue(metricName: string, value: number): void { const history = this.metricValues.get(metricName) || []; history.push({ value, timestamp: new Date() }); // Keep only last 24 hours of data const cutoff = Date.now() - 24 * 60 * 60 * 1000; const filteredHistory = history.filter( (h) => h.timestamp.getTime() > cutoff ); this.metricValues.set(metricName, filteredHistory); } // Process anomaly and create alert if needed processAnomaly(anomaly: Anomaly): void { const alert: Alert = { id: uuidv4(), timestamp: anomaly.timestamp, type: 'anomaly', severity: anomaly.severity, title: `Anomaly Detected: ${anomaly.metric}`, description: anomaly.description, sessionId: anomaly.sessionId, source: 'anomaly-detector', resolved: false, metadata: { anomalyId: anomaly.id, metric: anomaly.metric, value: anomaly.value, expectedValue: anomaly.expectedValue, deviation: anomaly.deviation, confidence: anomaly.confidence, type: anomaly.type, }, }; this.createAlert(alert); } // Create a new alert createAlert(alert: Alert): void { this.alerts.set(alert.id, alert); this.emit('alert-created', alert); // Send notifications based on alert severity and type this.sendNotifications(alert); this.logger.warn(`Alert created: ${alert.title} (${alert.severity})`); } // Resolve an alert resolveAlert(alertId: string, resolution?: string): void { const alert = this.alerts.get(alertId); if (!alert) { throw new Error(`Alert not found: ${alertId}`); } alert.resolved = true; alert.resolvedAt = new Date(); if (resolution) { alert.metadata = { ...alert.metadata, resolution }; } this.alerts.set(alertId, alert); this.emit('alert-resolved', alert); this.logger.info(`Alert resolved: ${alert.title}`); } // Evaluate all alert rules private evaluateRules(): void { this.alertRules.forEach((rule) => { if (rule.enabled) { this.evaluateRule(rule); } }); } // Evaluate a single alert rule private evaluateRule(rule: AlertRule): void { try { // Check cooldown period if (rule.lastTriggered) { const timeSinceLastTrigger = Date.now() - rule.lastTriggered.getTime(); const cooldownMs = rule.cooldownMinutes * 60 * 1000; if (timeSinceLastTrigger < cooldownMs) { return; // Still in cooldown } } // Evaluate all conditions const conditionsMet = rule.conditions.every((condition) => this.evaluateCondition(condition) ); if (conditionsMet) { this.triggerRule(rule); } } catch (error) { this.logger.error(`Error evaluating rule ${rule.name}: ${error}`); } } // Evaluate a single condition private evaluateCondition(condition: AlertCondition): boolean { const metricHistory = this.metricValues.get(condition.metric); if (!metricHistory || metricHistory.length === 0) { return false; } // Check if condition has been met for the required duration const durationMs = condition.duration * 60 * 1000; const cutoff = Date.now() - durationMs; const recentValues = metricHistory.filter( (h) => h.timestamp.getTime() > cutoff ); if (recentValues.length === 0) { return false; } // Check if all recent values meet the condition return recentValues.every((h) => this.compareValues(h.value, condition.operator, condition.threshold) ); } // Compare values based on operator private compareValues( value: number, operator: string, threshold: number ): boolean { switch (operator) { case 'gt': return value > threshold; case 'lt': return value < threshold; case 'eq': return value === threshold; case 'ne': return value !== threshold; case 'gte': return value >= threshold; case 'lte': return value <= threshold; default: return false; } } // Trigger an alert rule private triggerRule(rule: AlertRule): void { const alert: Alert = { id: uuidv4(), timestamp: new Date(), type: 'performance', severity: this.getSeverityFromRule(rule), title: rule.name, description: rule.description, source: 'alert-manager', resolved: false, metadata: { ruleId: rule.id, conditions: rule.conditions, }, }; rule.lastTriggered = new Date(); this.alertRules.set(rule.id, rule); this.createAlert(alert); } // Determine severity from rule private getSeverityFromRule( rule: AlertRule ): 'low' | 'medium' | 'high' | 'critical' { // Simple heuristic based on thresholds const hasHighThresholds = rule.conditions.some((c) => c.threshold > 90); const hasLowDuration = rule.conditions.some((c) => c.duration < 5); if (hasHighThresholds && hasLowDuration) return 'critical'; if (hasHighThresholds) return 'high'; if (hasLowDuration) return 'medium'; return 'low'; } // Send notifications for an alert private async sendNotifications(alert: Alert): Promise<void> { // Send notifications based on alert severity const relevantChannels = Array.from( this.notificationChannels.values() ).filter((channel) => channel.enabled); for (const channel of relevantChannels) { try { await this.sendNotification(channel, alert); } catch (error) { this.logger.error( `Failed to send notification via ${channel.type}: ${error}` ); } } } // Send notification through specific channel private async sendNotification( channel: NotificationChannel, alert: Alert ): Promise<void> { switch (channel.type) { case 'email': await this.sendEmailNotification(channel.config, alert); break; case 'webhook': await this.sendWebhookNotification(channel.config, alert); break; case 'slack': await this.sendSlackNotification(channel.config, alert); break; case 'console': this.sendConsoleNotification(alert); break; default: this.logger.warn(`Unknown notification channel type: ${channel.type}`); } } // Send email notification private async sendEmailNotification( config: any, alert: Alert ): Promise<void> { if (!config.smtp) { throw new Error('SMTP configuration required for email notifications'); } const transporter = nodemailer.createTransport(config.smtp); const subject = `[${alert.severity.toUpperCase()}] ${alert.title}`; const html = this.generateEmailHTML(alert); await transporter.sendMail({ from: config.from, to: config.to, subject, html, }); this.logger.info(`Email notification sent for alert: ${alert.title}`); } // Send webhook notification private async sendWebhookNotification( config: any, alert: Alert ): Promise<void> { const payload = { alert, timestamp: alert.timestamp.toISOString(), severity: alert.severity, title: alert.title, description: alert.description, }; await axios.post(config.url, payload, { headers: { 'Content-Type': 'application/json', ...config.headers, }, timeout: 10000, }); this.logger.info(`Webhook notification sent for alert: ${alert.title}`); } // Send Slack notification private async sendSlackNotification( config: any, alert: Alert ): Promise<void> { const color = this.getSlackColor(alert.severity); const payload = { text: `Alert: ${alert.title}`, attachments: [ { color, title: alert.title, text: alert.description, fields: [ { title: 'Severity', value: alert.severity.toUpperCase(), short: true, }, { title: 'Time', value: alert.timestamp.toISOString(), short: true, }, { title: 'Source', value: alert.source, short: true, }, ], footer: 'Console Automation MCP', ts: Math.floor(alert.timestamp.getTime() / 1000), }, ], }; await axios.post(config.webhookUrl, payload, { headers: { 'Content-Type': 'application/json' }, timeout: 10000, }); this.logger.info(`Slack notification sent for alert: ${alert.title}`); } // Send console notification private sendConsoleNotification(alert: Alert): void { const severity = alert.severity.toUpperCase(); const timestamp = alert.timestamp.toISOString(); console.log( `[${timestamp}] [ALERT-${severity}] ${alert.title}: ${alert.description}` ); } // Generate HTML for email notifications private generateEmailHTML(alert: Alert): string { const severityColor = this.getSeverityColor(alert.severity); return ` <html> <body style="font-family: Arial, sans-serif; margin: 20px;"> <div style="border-left: 4px solid ${severityColor}; padding-left: 20px;"> <h2 style="color: ${severityColor}; margin-top: 0;"> ${alert.title} </h2> <p><strong>Severity:</strong> ${alert.severity.toUpperCase()}</p> <p><strong>Time:</strong> ${alert.timestamp.toISOString()}</p> <p><strong>Source:</strong> ${alert.source}</p> ${alert.sessionId ? `<p><strong>Session ID:</strong> ${alert.sessionId}</p>` : ''} <p><strong>Description:</strong></p> <p>${alert.description}</p> ${ alert.metadata ? ` <details> <summary>Additional Details</summary> <pre>${JSON.stringify(alert.metadata, null, 2)}</pre> </details> ` : '' } </div> </body> </html> `; } // Get color for severity private getSeverityColor(severity: string): string { switch (severity) { case 'critical': return '#DC2626'; case 'high': return '#EA580C'; case 'medium': return '#D97706'; case 'low': return '#65A30D'; default: return '#6B7280'; } } // Get Slack color for severity private getSlackColor(severity: string): string { switch (severity) { case 'critical': return 'danger'; case 'high': return 'warning'; case 'medium': return '#D97706'; case 'low': return 'good'; default: return '#6B7280'; } } // Get all alerts getAlerts(): Alert[] { return Array.from(this.alerts.values()); } // Get active (unresolved) alerts getActiveAlerts(): Alert[] { return Array.from(this.alerts.values()).filter((alert) => !alert.resolved); } // Get alerts by severity getAlertsBySeverity( severity: 'low' | 'medium' | 'high' | 'critical' ): Alert[] { return Array.from(this.alerts.values()).filter( (alert) => alert.severity === severity ); } // Get alerts for time range getAlertsInTimeRange(startTime: Date, endTime: Date): Alert[] { return Array.from(this.alerts.values()).filter( (alert) => alert.timestamp >= startTime && alert.timestamp <= endTime ); } // Get alert statistics getStats(): { totalAlerts: number; activeAlerts: number; alertsBySeverity: Record<string, number>; alertsByType: Record<string, number>; rulesEnabled: number; rulesTotal: number; notificationChannels: number; } { const alerts = Array.from(this.alerts.values()); const activeAlerts = alerts.filter((a) => !a.resolved); const alertsBySeverity = { low: alerts.filter((a) => a.severity === 'low').length, medium: alerts.filter((a) => a.severity === 'medium').length, high: alerts.filter((a) => a.severity === 'high').length, critical: alerts.filter((a) => a.severity === 'critical').length, }; const alertsByType = { performance: alerts.filter((a) => a.type === 'performance').length, error: alerts.filter((a) => a.type === 'error').length, security: alerts.filter((a) => a.type === 'security').length, compliance: alerts.filter((a) => a.type === 'compliance').length, anomaly: alerts.filter((a) => a.type === 'anomaly').length, }; const rules = Array.from(this.alertRules.values()); return { totalAlerts: alerts.length, activeAlerts: activeAlerts.length, alertsBySeverity, alertsByType, rulesEnabled: rules.filter((r) => r.enabled).length, rulesTotal: rules.length, notificationChannels: this.notificationChannels.size, }; } // Clear old alerts to prevent memory leaks cleanupOldAlerts(maxAge: number = 7 * 24 * 60 * 60 * 1000): void { // 7 days default const cutoff = Date.now() - maxAge; Array.from(this.alerts.entries()).forEach(([id, alert]) => { if (alert.timestamp.getTime() < cutoff) { this.alerts.delete(id); } }); } destroy(): void { this.stop(); this.alerts.clear(); this.alertRules.clear(); this.notificationChannels.clear(); this.metricValues.clear(); this.removeAllListeners(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server