UNPKG

@sailboat-computer/event-bus

Version:

Standardized event bus for sailboat computer v3 with resilience features and offline capabilities

379 lines (334 loc) 10.2 kB
/** * Event bus monitoring and alerting */ import { v4 as uuidv4 } from 'uuid'; import { Alert, AlertHandler, AlertSeverity, AlertSeverityType, AlertSeverityMapping, AlertType, ExtendedEventBusMetrics } from './types'; import { logger } from './utils'; /** * Alert manager for event bus monitoring */ export class AlertManager { /** * Alert handlers */ private handlers = new Map<string, AlertHandler>(); /** * Active alerts */ private activeAlerts = new Map<string, Alert>(); /** * Alert history */ private alertHistory: Alert[] = []; /** * Maximum number of alerts to keep in history */ private maxHistorySize = 1000; /** * Alert thresholds */ private thresholds = { failedPublishesThreshold: 5, // 5% deadLetterQueueSizeThreshold: 10, reconnectionAttemptsThreshold: 3, processingTimeThreshold: 1000 // 1 second }; /** * Create a new alert manager * * @param thresholds - Alert thresholds */ constructor(thresholds?: { failedPublishesThreshold?: number; deadLetterQueueSizeThreshold?: number; reconnectionAttemptsThreshold?: number; processingTimeThreshold?: number; }) { if (thresholds) { this.thresholds = { ...this.thresholds, ...thresholds }; } } /** * Register an alert handler * * @param handler - Alert handler * @returns Handler ID */ registerHandler(handler: AlertHandler): string { const handlerId = uuidv4(); this.handlers.set(handlerId, handler); return handlerId; } /** * Unregister an alert handler * * @param handlerId - Handler ID */ unregisterHandler(handlerId: string): void { this.handlers.delete(handlerId); } /** * Check metrics for alerts * * @param metrics - Event bus metrics */ checkMetrics(metrics: ExtendedEventBusMetrics): void { // Check failed publishes if (metrics.publishedEvents > 0) { const failedPublishesPercentage = (metrics.failedPublishes / metrics.publishedEvents) * 100; if (failedPublishesPercentage >= this.thresholds.failedPublishesThreshold) { this.createOrUpdateAlert( AlertType.FAILED_PUBLISHES, failedPublishesPercentage >= this.thresholds.failedPublishesThreshold * 2 ? AlertSeverityMapping.ERROR : AlertSeverityMapping.WARNING, `Failed publishes rate of ${failedPublishesPercentage.toFixed(2)}% exceeds threshold of ${this.thresholds.failedPublishesThreshold}%`, { failedPublishes: metrics.failedPublishes, publishedEvents: metrics.publishedEvents, percentage: failedPublishesPercentage } ); } else { this.resolveAlert(AlertType.FAILED_PUBLISHES); } } // Check dead letter queue size if ('deadLetterQueueSize' in metrics && metrics.deadLetterQueueSize > 0) { if (metrics.deadLetterQueueSize >= this.thresholds.deadLetterQueueSizeThreshold) { this.createOrUpdateAlert( AlertType.DEAD_LETTER_QUEUE_SIZE, metrics.deadLetterQueueSize >= this.thresholds.deadLetterQueueSizeThreshold * 2 ? AlertSeverityMapping.ERROR : AlertSeverityMapping.WARNING, `Dead letter queue size of ${metrics.deadLetterQueueSize} exceeds threshold of ${this.thresholds.deadLetterQueueSizeThreshold}`, { deadLetterQueueSize: metrics.deadLetterQueueSize, deadLetterQueueEvents: (metrics as ExtendedEventBusMetrics).deadLetterQueueEvents } ); } else { this.resolveAlert(AlertType.DEAD_LETTER_QUEUE_SIZE); } } // Check reconnection attempts if (metrics.reconnectionAttempts > 0) { if (metrics.reconnectionAttempts >= this.thresholds.reconnectionAttemptsThreshold) { this.createOrUpdateAlert( AlertType.RECONNECTION_ATTEMPTS, metrics.reconnectionAttempts >= this.thresholds.reconnectionAttemptsThreshold * 2 ? AlertSeverityMapping.ERROR : AlertSeverityMapping.WARNING, `Reconnection attempts of ${metrics.reconnectionAttempts} exceeds threshold of ${this.thresholds.reconnectionAttemptsThreshold}`, { reconnectionAttempts: metrics.reconnectionAttempts, lastReconnectionTime: metrics.lastReconnectionTime } ); } else { this.resolveAlert(AlertType.RECONNECTION_ATTEMPTS); } } // Check processing time if (metrics.averageProcessingTime > 0) { if (metrics.averageProcessingTime >= this.thresholds.processingTimeThreshold) { this.createOrUpdateAlert( AlertType.PROCESSING_TIME, metrics.averageProcessingTime >= this.thresholds.processingTimeThreshold * 2 ? AlertSeverityMapping.ERROR : AlertSeverityMapping.WARNING, `Average processing time of ${metrics.averageProcessingTime.toFixed(2)}ms exceeds threshold of ${this.thresholds.processingTimeThreshold}ms`, { averageProcessingTime: metrics.averageProcessingTime } ); } else { this.resolveAlert(AlertType.PROCESSING_TIME); } } } /** * Check connection status * * @param connected - Whether the adapter is connected */ checkConnectionStatus(connected: boolean): void { if (!connected) { this.createOrUpdateAlert( AlertType.CONNECTION_STATUS, AlertSeverityMapping.ERROR, 'Event bus adapter is disconnected', { connected: false, timestamp: new Date() } ); } else { this.resolveAlert(AlertType.CONNECTION_STATUS); } } /** * Create or update an alert * * @param type - Alert type * @param severity - Alert severity * @param message - Alert message * @param details - Alert details */ private createOrUpdateAlert( type: AlertType, severity: AlertSeverityType, message: string, details?: Record<string, any> ): void { // Check if alert already exists const existingAlert = this.getActiveAlertByType(type); if (existingAlert) { // Update existing alert existingAlert.severity = severity; existingAlert.message = message; if (details) { existingAlert.details = details; } else if ('details' in existingAlert) { delete existingAlert.details; } logger.warn(`Updated alert: ${message}`); } else { // Create new alert const alert: Alert = { id: uuidv4(), type, severity, message, timestamp: new Date(), ...(details ? { details } : {}), resolved: false }; // Add to active alerts this.activeAlerts.set(alert.id, alert); // Add to history this.addToHistory(alert); // Notify handlers this.notifyHandlers(alert); logger.warn(`New alert: ${message}`); } } /** * Resolve an alert * * @param type - Alert type */ private resolveAlert(type: AlertType): void { // Find alert by type const alert = this.getActiveAlertByType(type); if (alert) { // Mark as resolved alert.resolved = true; alert.resolvedAt = new Date(); // Remove from active alerts this.activeAlerts.delete(alert.id); // Add to history this.addToHistory(alert); // Notify handlers this.notifyHandlers(alert); logger.info(`Resolved alert: ${alert.message}`); } } /** * Get active alert by type * * @param type - Alert type * @returns Alert or undefined */ private getActiveAlertByType(type: AlertType): Alert | undefined { for (const alert of this.activeAlerts.values()) { if (alert.type === type) { return alert; } } return undefined; } /** * Add alert to history * * @param alert - Alert */ private addToHistory(alert: Alert): void { this.alertHistory.push(alert); // Trim history if needed if (this.alertHistory.length > this.maxHistorySize) { this.alertHistory = this.alertHistory.slice(-this.maxHistorySize); } } /** * Notify handlers of an alert * * @param alert - Alert */ private notifyHandlers(alert: Alert): void { for (const handler of this.handlers.values()) { try { handler(alert); } catch (error) { logger.error(`Error in alert handler: ${error instanceof Error ? error.message : String(error)}`, error as Error); } } } /** * Get active alerts * * @param type - Optional alert type to filter by * @returns Active alerts */ getActiveAlerts(type?: AlertType): Alert[] { const alerts = Array.from(this.activeAlerts.values()); if (type) { return alerts.filter(alert => alert.type === type); } return alerts; } /** * Get alert history * * @param limit - Maximum number of alerts to return * @param type - Optional alert type to filter by * @returns Alert history */ getAlertHistory(limit?: number, type?: AlertType): Alert[] { let alerts = [...this.alertHistory]; // Filter by type if specified if (type) { alerts = alerts.filter(alert => alert.type === type); } // Sort by timestamp (newest first) alerts.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Limit if specified if (limit && limit > 0) { alerts = alerts.slice(0, limit); } return alerts; } /** * Clear alert history */ clearAlertHistory(): void { this.alertHistory = []; } } /** * Create a new alert manager * * @param thresholds - Alert thresholds * @returns Alert manager */ export function createAlertManager(thresholds?: { failedPublishesThreshold?: number; deadLetterQueueSizeThreshold?: number; reconnectionAttemptsThreshold?: number; processingTimeThreshold?: number; }): AlertManager { return new AlertManager(thresholds); }