@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
text/typescript
/**
* 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);
}