UNPKG

@sailboat-computer/health-monitoring

Version:

Comprehensive health monitoring system for sailboat computer v3 with marine-specific health checks

554 lines (485 loc) 15.3 kB
/** * Core health check engine for marine systems */ import { HealthCheckResult, HealthCheckConfig, HealthCheckType, HealthStatus, HealthStatusType, OperationalContext, OperationalContextType, MarineSystemType, MarineSystemTypeType, MarineEnvironmentStatus, HealthMonitoringEvent } from '../types'; import { executeWithTimeout, TimeoutContexts } from '@sailboat-computer/resilience'; /** * Abstract base class for health checks */ export abstract class BaseHealthCheck { protected config: HealthCheckConfig; protected lastResult?: HealthCheckResult; protected consecutiveFailures = 0; protected lastExecutionTime = 0; constructor(config: HealthCheckConfig) { this.config = config; } /** * Execute the health check */ async execute( operationalContext: OperationalContextType, marineEnvironment?: MarineEnvironmentStatus ): Promise<HealthCheckResult> { const startTime = Date.now(); try { // Check if this health check should run in current operational context if (!this.shouldExecute(operationalContext, marineEnvironment)) { return this.createSkippedResult(operationalContext, 'Skipped due to operational context'); } // Execute the actual health check with timeout protection const timeoutContext = this.getTimeoutContext(operationalContext); const result = await executeWithTimeout( () => this.performCheck(operationalContext, marineEnvironment), timeoutContext ); if (result.success && result.data) { this.consecutiveFailures = 0; this.lastResult = result.data; this.lastExecutionTime = Date.now() - startTime; return result.data; } else { throw result.error || new Error('Health check failed'); } } catch (error) { this.consecutiveFailures++; const failureResult = this.createFailureResult( operationalContext, error as Error, Date.now() - startTime ); this.lastResult = failureResult; this.lastExecutionTime = Date.now() - startTime; return failureResult; } } /** * Get the last health check result */ getLastResult(): HealthCheckResult | undefined { return this.lastResult; } /** * Get health check configuration */ getConfig(): HealthCheckConfig { return { ...this.config }; } /** * Check if health check should execute in current context */ protected shouldExecute( operationalContext: OperationalContextType, marineEnvironment?: MarineEnvironmentStatus ): boolean { // Check operational context if (!this.config.marineSettings.operationalContexts.includes(operationalContext)) { return false; } // Check power awareness if (this.config.marineSettings.powerAware && marineEnvironment?.powerStatus === 'critical') { // Only run critical health checks when power is critical return this.config.marineSystemType === MarineSystemType.SAFETY || this.config.marineSystemType === MarineSystemType.NAVIGATION; } // Check environmental conditions if (marineEnvironment?.criticalOperationsOnly && !this.config.marineSettings.safetyImpact) { return false; } return true; } /** * Get timeout context for this health check */ protected getTimeoutContext(operationalContext: OperationalContextType) { switch (this.config.marineSystemType) { case MarineSystemType.SAFETY: return TimeoutContexts.safetyOperation(this.config.name, operationalContext); case MarineSystemType.NAVIGATION: return TimeoutContexts.navigationSensor(this.config.name, operationalContext); case MarineSystemType.COMMUNICATION: return TimeoutContexts.networkOperation(this.config.name, operationalContext); case MarineSystemType.MAINTENANCE: return TimeoutContexts.maintenanceOperation(this.config.name); default: return TimeoutContexts.comfortOperation(this.config.name, operationalContext); } } /** * Create a skipped result */ protected createSkippedResult( operationalContext: OperationalContextType, reason: string ): HealthCheckResult { return { checkId: this.config.checkId, name: this.config.name, type: this.config.type, marineSystemType: this.config.marineSystemType, status: HealthStatus.UNKNOWN, score: 0.5, // Neutral score for skipped checks message: reason, timestamp: new Date(), executionTime: 0, marineContext: { operationalContext, environmentalImpact: false, safetyImpact: this.config.marineSettings.safetyImpact, powerImpact: 0 } }; } /** * Create a failure result */ protected createFailureResult( operationalContext: OperationalContextType, error: Error, executionTime: number ): HealthCheckResult { // Determine severity based on consecutive failures let status: HealthStatusType; let score: number; if (this.consecutiveFailures >= 3) { status = HealthStatus.CRITICAL; score = 0.0; } else if (this.consecutiveFailures >= 2) { status = HealthStatus.UNHEALTHY; score = 0.2; } else { status = HealthStatus.DEGRADED; score = 0.4; } return { checkId: this.config.checkId, name: this.config.name, type: this.config.type, marineSystemType: this.config.marineSystemType, status, score, message: `Health check failed: ${error.message}`, details: { error: error.message, consecutiveFailures: this.consecutiveFailures, stack: error.stack }, timestamp: new Date(), executionTime, marineContext: { operationalContext, environmentalImpact: false, safetyImpact: this.config.marineSettings.safetyImpact, powerImpact: 0 } }; } /** * Abstract method to perform the actual health check */ protected abstract performCheck( operationalContext: OperationalContextType, marineEnvironment?: MarineEnvironmentStatus ): Promise<HealthCheckResult>; } /** * Health check scheduler and executor */ export class HealthCheckEngine { private healthChecks: Map<string, BaseHealthCheck> = new Map(); private scheduledChecks: Map<string, NodeJS.Timeout> = new Map(); private eventListeners: ((event: HealthMonitoringEvent) => void)[] = []; private isRunning = false; private currentOperationalContext: OperationalContextType = OperationalContext.DOCKED; private currentMarineEnvironment?: MarineEnvironmentStatus; /** * Register a health check */ registerHealthCheck(healthCheck: BaseHealthCheck): void { const config = healthCheck.getConfig(); this.healthChecks.set(config.checkId, healthCheck); // Schedule the health check if engine is running if (this.isRunning) { this.scheduleHealthCheck(config.checkId); } this.emitEvent('health_check_completed', config.checkId, { message: `Health check '${config.name}' registered`, details: { action: 'registered' } }); } /** * Unregister a health check */ unregisterHealthCheck(checkId: string): void { const healthCheck = this.healthChecks.get(checkId); if (!healthCheck) { return; } // Cancel scheduled execution const timer = this.scheduledChecks.get(checkId); if (timer) { clearTimeout(timer); this.scheduledChecks.delete(checkId); } this.healthChecks.delete(checkId); this.emitEvent('health_check_completed', checkId, { message: `Health check unregistered`, details: { action: 'unregistered' } }); } /** * Start the health check engine */ start(): void { if (this.isRunning) { return; } this.isRunning = true; // Schedule all registered health checks for (const checkId of this.healthChecks.keys()) { this.scheduleHealthCheck(checkId); } this.emitEvent('system_recovery', 'health-engine', { message: 'Health check engine started', details: { registeredChecks: this.healthChecks.size, operationalContext: this.currentOperationalContext } }); } /** * Stop the health check engine */ stop(): void { if (!this.isRunning) { return; } this.isRunning = false; // Cancel all scheduled checks for (const [checkId, timer] of this.scheduledChecks) { clearTimeout(timer); } this.scheduledChecks.clear(); this.emitEvent('system_recovery', 'health-engine', { message: 'Health check engine stopped', details: { cancelledChecks: this.scheduledChecks.size } }); } /** * Update operational context */ updateOperationalContext(context: OperationalContextType): void { const previousContext = this.currentOperationalContext; this.currentOperationalContext = context; this.emitEvent('system_recovery', 'health-engine', { message: 'Operational context updated', details: { previousContext, newContext: context, affectedChecks: this.getAffectedChecks(context) } }); // Reschedule health checks if context change affects them this.rescheduleAffectedChecks(context); } /** * Update marine environment status */ updateMarineEnvironment(environment: MarineEnvironmentStatus): void { this.currentMarineEnvironment = environment; this.emitEvent('system_recovery', 'health-engine', { message: 'Marine environment updated', details: { seaState: environment.seaState, weather: environment.weather, powerStatus: environment.powerStatus, criticalOperationsOnly: environment.criticalOperationsOnly } }); // Adjust health check scheduling based on environment this.adjustForEnvironment(environment); } /** * Execute a specific health check immediately */ async executeHealthCheck(checkId: string): Promise<HealthCheckResult | null> { const healthCheck = this.healthChecks.get(checkId); if (!healthCheck) { return null; } try { const result = await healthCheck.execute( this.currentOperationalContext, this.currentMarineEnvironment ); this.emitEvent('health_check_completed', checkId, { message: `Health check completed: ${result.status}`, score: result.score, details: { status: result.status, executionTime: result.executionTime, manual: true } }); return result; } catch (error) { this.emitEvent('alert_triggered', checkId, { message: `Health check execution failed: ${(error as Error).message}`, details: { error: (error as Error).message } }); return null; } } /** * Get all health check results */ getAllResults(): Map<string, HealthCheckResult> { const results = new Map<string, HealthCheckResult>(); for (const [checkId, healthCheck] of this.healthChecks) { const result = healthCheck.getLastResult(); if (result) { results.set(checkId, result); } } return results; } /** * Get health checks by system type */ getHealthChecksBySystem(systemType: MarineSystemTypeType): BaseHealthCheck[] { return Array.from(this.healthChecks.values()).filter( check => check.getConfig().marineSystemType === systemType ); } /** * Add event listener */ onEvent(listener: (event: HealthMonitoringEvent) => void): void { this.eventListeners.push(listener); } /** * Remove event listener */ removeEventListener(listener: (event: HealthMonitoringEvent) => void): void { const index = this.eventListeners.indexOf(listener); if (index > -1) { this.eventListeners.splice(index, 1); } } /** * Schedule a health check */ private scheduleHealthCheck(checkId: string): void { const healthCheck = this.healthChecks.get(checkId); if (!healthCheck) { return; } const config = healthCheck.getConfig(); // Cancel existing timer const existingTimer = this.scheduledChecks.get(checkId); if (existingTimer) { clearTimeout(existingTimer); } // Calculate interval based on marine environment let interval = config.interval; if (this.currentMarineEnvironment) { // Adjust interval based on environmental conditions if (this.currentMarineEnvironment.criticalOperationsOnly) { interval *= 0.5; // More frequent checks in critical conditions } else if (this.currentMarineEnvironment.powerStatus === 'conservation') { interval *= 2.0; // Less frequent checks to save power } } // Schedule next execution const timer = setTimeout(async () => { await this.executeHealthCheck(checkId); // Reschedule if still running if (this.isRunning) { this.scheduleHealthCheck(checkId); } }, interval); this.scheduledChecks.set(checkId, timer); } /** * Get health checks affected by operational context change */ private getAffectedChecks(context: OperationalContextType): string[] { const affected: string[] = []; for (const [checkId, healthCheck] of this.healthChecks) { const config = healthCheck.getConfig(); if (!config.marineSettings.operationalContexts.includes(context)) { affected.push(checkId); } } return affected; } /** * Reschedule health checks affected by context change */ private rescheduleAffectedChecks(context: OperationalContextType): void { for (const checkId of this.healthChecks.keys()) { this.scheduleHealthCheck(checkId); } } /** * Adjust health check scheduling for marine environment */ private adjustForEnvironment(environment: MarineEnvironmentStatus): void { // Reschedule all checks with new environmental considerations for (const checkId of this.healthChecks.keys()) { this.scheduleHealthCheck(checkId); } } /** * Emit health monitoring event */ private emitEvent( eventType: HealthMonitoringEvent['eventType'], checkId: string, data: { message: string; score?: number; details?: Record<string, any>; } ): void { const event: HealthMonitoringEvent = { eventId: `${eventType}-${checkId}-${Date.now()}`, timestamp: new Date(), eventType, systemId: 'health-engine', checkId, currentStatus: HealthStatus.UNKNOWN, // Will be updated by specific implementations data: { ...data, marineContext: { operationalContext: this.currentOperationalContext, environmentalConditions: this.currentMarineEnvironment ? { ...this.currentMarineEnvironment } : {}, safetyImpact: false } } }; this.eventListeners.forEach(listener => { try { listener(event); } catch (error) { console.error('Error in health monitoring event listener:', error); } }); } } /** * Default health check engine instance */ export const defaultHealthCheckEngine = new HealthCheckEngine();