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