Skip to main content
Glama
ooples

MCP Console Automation Server

HeartbeatMonitor.ts34.3 kB
import { EventEmitter } from 'events'; import { Logger } from '../utils/logger.js'; import { SessionState } from '../types/index.js'; export interface HeartbeatConfig { interval: number; timeout: number; maxMissedBeats: number; enableAdaptiveInterval: boolean; retryAttempts: number; retryDelay: number; gracePeriod: number; enablePredictiveFailure: boolean; sshHeartbeatInterval: number; sshTimeoutThreshold: number; enableSSHProactiveReconnect: boolean; sshFailureRiskThreshold: number; } export interface HeartbeatResult { sessionId: string; timestamp: Date; success: boolean; responseTime: number; error?: string; consecutiveMissed: number; adaptiveInterval?: number; } export interface SessionHeartbeat { sessionId: string; lastBeat: Date; consecutiveMissed: number; averageResponseTime: number; responseTimeHistory: number[]; isHealthy: boolean; adaptiveInterval: number; nextHeartbeat: Date; failurePredictionScore?: number; isSSHSession?: boolean; sshHealthData?: SSHHeartbeatData; } export interface SSHHeartbeatData { hostname: string; port: number; username: string; lastKeepAliveSuccess: Date; keepAliveSuccessRate: number; connectionStability: number; networkLatency: number; authenticationStatus: 'valid' | 'expired' | 'failed' | 'unknown'; encryptionStrength: 'weak' | 'medium' | 'strong' | 'unknown'; dataTransferRate: number; commandExecutionTime: number; proactiveReconnectTriggered: boolean; riskFactors: { networkDegradation: number; performanceIssues: number; authenticationRisk: number; connectionAging: number; }; } /** * Heartbeat Monitor for session health verification * Provides continuous monitoring of session responsiveness with adaptive intervals * and predictive failure detection */ export class HeartbeatMonitor extends EventEmitter { private logger: Logger; private config: HeartbeatConfig; private sessionHeartbeats: Map<string, SessionHeartbeat> = new Map(); private heartbeatTimers: Map<string, NodeJS.Timeout> = new Map(); private activeHeartbeats: Map<string, Promise<HeartbeatResult>> = new Map(); private isRunning = false; // Statistics private stats = { totalHeartbeats: 0, successfulHeartbeats: 0, failedHeartbeats: 0, totalSessions: 0, activeSessions: 0, unhealthySessions: 0, averageResponseTime: 0, predictedFailures: 0, preventedFailures: 0, }; constructor(config?: Partial<HeartbeatConfig>) { super(); this.logger = new Logger('HeartbeatMonitor'); this.config = { interval: config?.interval || 30000, // 30 seconds timeout: config?.timeout || 10000, // 10 seconds maxMissedBeats: config?.maxMissedBeats || 3, enableAdaptiveInterval: config?.enableAdaptiveInterval ?? true, retryAttempts: config?.retryAttempts || 2, retryDelay: config?.retryDelay || 5000, gracePeriod: config?.gracePeriod || 60000, // 1 minute grace period for new sessions enablePredictiveFailure: config?.enablePredictiveFailure ?? true, sshHeartbeatInterval: config?.sshHeartbeatInterval || 20000, // 20 seconds for SSH sshTimeoutThreshold: config?.sshTimeoutThreshold || 15000, // 15 seconds SSH timeout enableSSHProactiveReconnect: config?.enableSSHProactiveReconnect ?? true, sshFailureRiskThreshold: config?.sshFailureRiskThreshold || 0.7, }; this.logger.info('HeartbeatMonitor initialized with config:', this.config); } /** * Start heartbeat monitoring */ async start(): Promise<void> { if (this.isRunning) { this.logger.warn('HeartbeatMonitor is already running'); return; } this.logger.info('Starting HeartbeatMonitor...'); this.isRunning = true; this.emit('started'); this.logger.info('HeartbeatMonitor started successfully'); } /** * Stop heartbeat monitoring */ async stop(): Promise<void> { if (!this.isRunning) { this.logger.warn('HeartbeatMonitor is not running'); return; } this.logger.info('Stopping HeartbeatMonitor...'); this.isRunning = false; // Clear all timers this.heartbeatTimers.forEach((timer, sessionId) => { clearTimeout(timer); }); this.heartbeatTimers.clear(); // Wait for active heartbeats to complete const activePromises = Array.from(this.activeHeartbeats.values()); if (activePromises.length > 0) { this.logger.info( `Waiting for ${activePromises.length} active heartbeats to complete...` ); await Promise.allSettled(activePromises); } this.emit('stopped'); this.logger.info('HeartbeatMonitor stopped'); } /** * Add a session for heartbeat monitoring */ addSession( sessionId: string, sessionState?: SessionState, sshConnectionInfo?: { hostname: string; port: number; username: string; } ): void { if (this.sessionHeartbeats.has(sessionId)) { this.logger.warn(`Session ${sessionId} is already being monitored`); return; } const now = Date.now(); const isSSH = sessionState?.type === 'ssh' || !!sshConnectionInfo; // Use SSH-specific interval for SSH sessions const baseInterval = isSSH ? this.config.sshHeartbeatInterval : this.config.interval; const heartbeat: SessionHeartbeat = { sessionId, lastBeat: new Date(now), consecutiveMissed: 0, averageResponseTime: 0, responseTimeHistory: [], isHealthy: true, adaptiveInterval: baseInterval, nextHeartbeat: new Date(now + this.config.gracePeriod), // Give grace period for new sessions isSSHSession: isSSH, }; // Initialize SSH-specific data if this is an SSH session if (isSSH && sshConnectionInfo) { heartbeat.sshHealthData = { hostname: sshConnectionInfo.hostname, port: sshConnectionInfo.port, username: sshConnectionInfo.username, lastKeepAliveSuccess: new Date(), keepAliveSuccessRate: 1.0, connectionStability: 1.0, networkLatency: 0, authenticationStatus: 'valid', encryptionStrength: 'unknown', dataTransferRate: 0, commandExecutionTime: 0, proactiveReconnectTriggered: false, riskFactors: { networkDegradation: 0, performanceIssues: 0, authenticationRisk: 0, connectionAging: 0, }, }; } this.sessionHeartbeats.set(sessionId, heartbeat); this.stats.totalSessions++; this.stats.activeSessions++; this.logger.info( `Added ${isSSH ? 'SSH ' : ''}session ${sessionId} for heartbeat monitoring${ sshConnectionInfo ? ` (${sshConnectionInfo.hostname}:${sshConnectionInfo.port})` : '' }` ); // Schedule first heartbeat after grace period this.scheduleHeartbeat(sessionId, this.config.gracePeriod); this.emit('session-added', { sessionId, heartbeat, isSSH }); } /** * Remove a session from heartbeat monitoring */ removeSession(sessionId: string): void { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { this.logger.warn(`Session ${sessionId} is not being monitored`); return; } // Clear timer const timer = this.heartbeatTimers.get(sessionId); if (timer) { clearTimeout(timer); this.heartbeatTimers.delete(sessionId); } // Cancel active heartbeat if running this.activeHeartbeats.delete(sessionId); this.sessionHeartbeats.delete(sessionId); this.stats.activeSessions--; if (!heartbeat.isHealthy) { this.stats.unhealthySessions--; } this.logger.info(`Removed session ${sessionId} from heartbeat monitoring`); this.emit('session-removed', { sessionId }); } /** * Get heartbeat status for a session */ getSessionHeartbeat(sessionId: string): SessionHeartbeat | null { return this.sessionHeartbeats.get(sessionId) || null; } /** * Get heartbeat status for all sessions */ getAllHeartbeats(): Record<string, SessionHeartbeat> { const result: Record<string, SessionHeartbeat> = {}; this.sessionHeartbeats.forEach((heartbeat, sessionId) => { result[sessionId] = { ...heartbeat }; }); return result; } /** * Force a heartbeat check for a specific session */ async forceHeartbeat(sessionId: string): Promise<HeartbeatResult> { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { throw new Error(`Session ${sessionId} is not being monitored`); } this.logger.debug(`Forcing heartbeat for session ${sessionId}`); return await this.performHeartbeat(sessionId); } /** * Update session activity (resets heartbeat timer) */ recordSessionActivity(sessionId: string): void { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { return; } heartbeat.lastBeat = new Date(); heartbeat.consecutiveMissed = 0; // Reset unhealthy status if session becomes active if (!heartbeat.isHealthy) { heartbeat.isHealthy = true; this.stats.unhealthySessions--; this.logger.info(`Session ${sessionId} recovered - marked as healthy`); this.emit('session-recovered', { sessionId, heartbeat }); } // Reschedule next heartbeat this.scheduleHeartbeat(sessionId); } /** * Schedule the next heartbeat for a session */ private scheduleHeartbeat(sessionId: string, customInterval?: number): void { if (!this.isRunning) { return; } const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { return; } // Clear existing timer const existingTimer = this.heartbeatTimers.get(sessionId); if (existingTimer) { clearTimeout(existingTimer); } // Calculate interval (adaptive or fixed) const interval = customInterval || this.calculateAdaptiveInterval(heartbeat); heartbeat.nextHeartbeat = new Date(Date.now() + interval); // Schedule heartbeat const timer = setTimeout(async () => { try { await this.performHeartbeat(sessionId); } catch (error) { this.logger.error( `Error performing heartbeat for session ${sessionId}:`, error ); } }, interval); this.heartbeatTimers.set(sessionId, timer); } /** * Calculate adaptive heartbeat interval based on session health */ private calculateAdaptiveInterval(heartbeat: SessionHeartbeat): number { if (!this.config.enableAdaptiveInterval) { return heartbeat.isSSHSession ? this.config.sshHeartbeatInterval : this.config.interval; } const baseInterval = heartbeat.isSSHSession ? this.config.sshHeartbeatInterval : this.config.interval; let multiplier = 1.0; // Increase frequency for unhealthy sessions if (!heartbeat.isHealthy) { multiplier = 0.5; // Check twice as often } else if (heartbeat.consecutiveMissed > 0) { multiplier = 0.75; // Check more frequently if there are recent misses } else if (heartbeat.averageResponseTime > 5000) { multiplier = 0.8; // Check more frequently for slow sessions } else if ( heartbeat.responseTimeHistory.length >= 10 && heartbeat.averageResponseTime < 1000 ) { // Decrease frequency for very healthy sessions multiplier = 1.5; } // SSH-specific adaptive logic if (heartbeat.isSSHSession && heartbeat.sshHealthData) { const sshData = heartbeat.sshHealthData; // Check more frequently for SSH sessions with high risk factors const totalRisk = sshData.riskFactors.networkDegradation + sshData.riskFactors.performanceIssues + sshData.riskFactors.authenticationRisk + sshData.riskFactors.connectionAging; if (totalRisk > 2.0) { multiplier *= 0.4; // Very high risk - check very frequently } else if (totalRisk > 1.0) { multiplier *= 0.6; // High risk - check more frequently } // Adjust based on connection stability if (sshData.connectionStability < 0.5) { multiplier *= 0.5; // Unstable connection - check more frequently } else if ( sshData.connectionStability > 0.9 && sshData.keepAliveSuccessRate > 0.95 ) { multiplier *= 1.3; // Very stable connection - check less frequently } // Adjust based on network latency if (sshData.networkLatency > this.config.sshTimeoutThreshold) { multiplier *= 0.7; // High latency - check more frequently } // Proactive reconnection logic if ( this.config.enableSSHProactiveReconnect && !sshData.proactiveReconnectTriggered ) { const failureRisk = this.calculateSSHFailureRisk(heartbeat); if (failureRisk >= this.config.sshFailureRiskThreshold) { // Trigger proactive reconnection this.triggerProactiveReconnect(heartbeat.sessionId, failureRisk); sshData.proactiveReconnectTriggered = true; } } } // Apply predictive failure analysis if ( this.config.enablePredictiveFailure && heartbeat.failurePredictionScore !== undefined ) { if (heartbeat.failurePredictionScore > 0.7) { multiplier *= 0.5; // High failure risk - check very frequently } else if (heartbeat.failurePredictionScore > 0.4) { multiplier *= 0.75; // Medium failure risk - check more frequently } } const adaptiveInterval = Math.floor(baseInterval * multiplier); heartbeat.adaptiveInterval = adaptiveInterval; // SSH sessions have tighter bounds const minInterval = heartbeat.isSSHSession ? 3000 : 5000; // 3-5 seconds minimum const maxInterval = heartbeat.isSSHSession ? 120000 : 300000; // 2-5 minutes maximum return Math.max(minInterval, Math.min(adaptiveInterval, maxInterval)); } /** * Perform heartbeat check for a session */ private async performHeartbeat(sessionId: string): Promise<HeartbeatResult> { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { throw new Error(`Session ${sessionId} not found`); } // Check if there's already an active heartbeat for this session const activeHeartbeat = this.activeHeartbeats.get(sessionId); if (activeHeartbeat) { this.logger.debug( `Heartbeat already in progress for session ${sessionId}` ); return await activeHeartbeat; } const heartbeatPromise = this.executeHeartbeat(sessionId, heartbeat); this.activeHeartbeats.set(sessionId, heartbeatPromise); try { const result = await heartbeatPromise; this.processHeartbeatResult(sessionId, result, heartbeat); this.scheduleHeartbeat(sessionId); return result; } finally { this.activeHeartbeats.delete(sessionId); } } /** * Execute the actual heartbeat check */ private async executeHeartbeat( sessionId: string, heartbeat: SessionHeartbeat ): Promise<HeartbeatResult> { const startTime = Date.now(); let attempt = 0; let lastError: string | undefined; this.stats.totalHeartbeats++; // Try multiple attempts with retry delay while (attempt < this.config.retryAttempts) { attempt++; try { // Emit heartbeat attempt this.emit('heartbeat-attempt', { sessionId, attempt, maxAttempts: this.config.retryAttempts, timestamp: new Date(), }); // Simulate heartbeat check - in reality, this would ping the session const heartbeatResult = await this.checkSessionHealth(sessionId); const responseTime = Date.now() - startTime; this.stats.successfulHeartbeats++; return { sessionId, timestamp: new Date(), success: true, responseTime, consecutiveMissed: heartbeat.consecutiveMissed, }; } catch (error) { lastError = error instanceof Error ? error.message : String(error); if (attempt < this.config.retryAttempts) { this.logger.debug( `Heartbeat attempt ${attempt} failed for session ${sessionId}, retrying in ${this.config.retryDelay}ms: ${lastError}` ); await this.delay(this.config.retryDelay); } } } // All attempts failed this.stats.failedHeartbeats++; const responseTime = Date.now() - startTime; return { sessionId, timestamp: new Date(), success: false, responseTime, error: lastError, consecutiveMissed: heartbeat.consecutiveMissed + 1, }; } /** * Check session health (placeholder for actual implementation) */ private async checkSessionHealth(sessionId: string): Promise<void> { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat) { throw new Error(`Session ${sessionId} not found`); } const timeout = heartbeat.isSSHSession ? this.config.sshTimeoutThreshold : this.config.timeout; return new Promise((resolve, reject) => { // Emit request for session health check this.emit('heartbeat-check-request', { sessionId, timeout, isSSHSession: heartbeat.isSSHSession, timestamp: new Date(), callback: (error?: Error, healthData?: any) => { if (error) { reject(error); } else { // Update SSH health data if available if ( heartbeat.isSSHSession && heartbeat.sshHealthData && healthData ) { this.updateSSHHealthData(heartbeat, healthData); } resolve(); } }, }); // Timeout mechanism setTimeout(() => { reject(new Error(`Heartbeat timeout after ${timeout}ms`)); }, timeout); }); } /** * Process heartbeat result and update session state */ private processHeartbeatResult( sessionId: string, result: HeartbeatResult, heartbeat: SessionHeartbeat ): void { heartbeat.lastBeat = result.timestamp; if (result.success) { // Successful heartbeat heartbeat.consecutiveMissed = 0; // Update response time statistics heartbeat.responseTimeHistory.push(result.responseTime); if (heartbeat.responseTimeHistory.length > 50) { heartbeat.responseTimeHistory.shift(); // Keep only last 50 measurements } heartbeat.averageResponseTime = heartbeat.responseTimeHistory.reduce((a, b) => a + b, 0) / heartbeat.responseTimeHistory.length; // Update global average this.stats.averageResponseTime = (this.stats.averageResponseTime * (this.stats.successfulHeartbeats - 1) + result.responseTime) / this.stats.successfulHeartbeats; // Mark as healthy if was unhealthy if (!heartbeat.isHealthy) { heartbeat.isHealthy = true; this.stats.unhealthySessions--; this.logger.info(`Session ${sessionId} heartbeat recovered`); this.emit('session-recovered', { sessionId, result, heartbeat }); } // Update failure prediction score if (this.config.enablePredictiveFailure) { heartbeat.failurePredictionScore = this.calculateFailurePrediction(heartbeat); // Emit prediction warning if high risk if (heartbeat.failurePredictionScore > 0.7) { this.stats.predictedFailures++; this.emit('failure-prediction', { sessionId, score: heartbeat.failurePredictionScore, recommendations: this.generatePredictiveRecommendations(heartbeat), timestamp: new Date(), }); } } } else { // Failed heartbeat heartbeat.consecutiveMissed = result.consecutiveMissed; // Mark as unhealthy if exceeds threshold if ( heartbeat.consecutiveMissed >= this.config.maxMissedBeats && heartbeat.isHealthy ) { heartbeat.isHealthy = false; this.stats.unhealthySessions++; this.logger.warn( `Session ${sessionId} marked as unhealthy after ${heartbeat.consecutiveMissed} consecutive missed heartbeats` ); this.emit('session-unhealthy', { sessionId, result, heartbeat, consecutiveMissed: heartbeat.consecutiveMissed, }); } // Emit failure event if session is critically unhealthy if (heartbeat.consecutiveMissed >= this.config.maxMissedBeats * 2) { this.emit('session-critical', { sessionId, result, heartbeat, message: `Session has missed ${heartbeat.consecutiveMissed} consecutive heartbeats`, }); } } // Emit heartbeat result this.emit('heartbeat-result', result); this.logger.debug( `Heartbeat for session ${sessionId}: ${result.success ? 'SUCCESS' : 'FAILED'} (${result.responseTime}ms)` ); } /** * Calculate failure prediction score based on historical data */ private calculateFailurePrediction(heartbeat: SessionHeartbeat): number { let score = 0; // Recent missed heartbeats increase failure risk if (heartbeat.consecutiveMissed > 0) { score += heartbeat.consecutiveMissed * 0.2; } // Degrading response times indicate potential issues if (heartbeat.responseTimeHistory.length >= 10) { const recent = heartbeat.responseTimeHistory.slice(-5); const older = heartbeat.responseTimeHistory.slice(-10, -5); const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length; const olderAvg = older.reduce((a, b) => a + b, 0) / older.length; if (recentAvg > olderAvg * 1.5) { score += 0.3; // Response time degradation } if (recentAvg > 10000) { score += 0.2; // Very slow responses } } // High variability in response times indicates instability if (heartbeat.responseTimeHistory.length >= 5) { const avg = heartbeat.averageResponseTime; const variance = heartbeat.responseTimeHistory .map((rt) => Math.pow(rt - avg, 2)) .reduce((a, b) => a + b, 0) / heartbeat.responseTimeHistory.length; const stdDev = Math.sqrt(variance); if (stdDev > avg * 0.5) { score += 0.15; // High variability } } return Math.min(1.0, score); } /** * Generate predictive recommendations based on failure analysis */ private generatePredictiveRecommendations( heartbeat: SessionHeartbeat ): string[] { const recommendations: string[] = []; if (heartbeat.consecutiveMissed > 0) { recommendations.push('Monitor session connectivity closely'); recommendations.push('Consider preemptive session restart'); } if (heartbeat.averageResponseTime > 5000) { recommendations.push('Investigate session performance issues'); recommendations.push('Check system resource utilization'); } if (heartbeat.responseTimeHistory.length >= 5) { const recentTrend = this.calculateResponseTimeTrend( heartbeat.responseTimeHistory ); if (recentTrend > 1.2) { recommendations.push( 'Response times are degrading - investigate root cause' ); recommendations.push('Consider session optimization or restart'); } } return recommendations; } /** * Calculate response time trend (1.0 = stable, >1.0 = degrading, <1.0 = improving) */ private calculateResponseTimeTrend(history: number[]): number { if (history.length < 4) return 1.0; const mid = Math.floor(history.length / 2); const firstHalf = history.slice(0, mid); const secondHalf = history.slice(mid); const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length; const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length; return secondAvg / firstAvg; } /** * Calculate SSH failure risk */ private calculateSSHFailureRisk(heartbeat: SessionHeartbeat): number { if (!heartbeat.isSSHSession || !heartbeat.sshHealthData) { return 0; } const sshData = heartbeat.sshHealthData; let riskScore = 0; // Factor in connection stability riskScore += (1 - sshData.connectionStability) * 0.3; // Factor in keep-alive success rate riskScore += (1 - sshData.keepAliveSuccessRate) * 0.25; // Factor in network latency if (sshData.networkLatency > this.config.sshTimeoutThreshold) { riskScore += 0.2; } // Factor in authentication status switch (sshData.authenticationStatus) { case 'failed': riskScore += 0.4; break; case 'expired': riskScore += 0.3; break; case 'unknown': riskScore += 0.1; break; } // Factor in individual risk factors const totalRiskFactors = Object.values(sshData.riskFactors).reduce( (sum, risk) => sum + risk, 0 ); riskScore += Math.min(0.4, (totalRiskFactors / 4) * 0.4); // Factor in consecutive missed heartbeats if (heartbeat.consecutiveMissed > 0) { riskScore += Math.min(0.2, heartbeat.consecutiveMissed * 0.05); } return Math.min(1.0, riskScore); } /** * Trigger proactive reconnection */ private triggerProactiveReconnect( sessionId: string, failureRisk: number ): void { const heartbeat = this.sessionHeartbeats.get(sessionId); if (!heartbeat || !heartbeat.isSSHSession) { return; } this.logger.warn( `Triggering proactive reconnection for SSH session ${sessionId} (risk: ${(failureRisk * 100).toFixed(1)}%)` ); // Emit proactive reconnection event this.emit('ssh-proactive-reconnect', { sessionId, failureRisk, heartbeat, timestamp: new Date(), reason: 'predictive-failure-prevention', urgency: failureRisk > 0.8 ? 'high' : 'medium', }); // Update statistics this.stats.preventedFailures++; // Log the event this.logger.info( `Proactive reconnection triggered for session ${sessionId} with risk score ${failureRisk.toFixed(3)}` ); } /** * Update SSH health data from heartbeat response */ private updateSSHHealthData( heartbeat: SessionHeartbeat, healthData: any ): void { if (!heartbeat.sshHealthData) { return; } const sshData = heartbeat.sshHealthData; // Update basic metrics if (healthData.networkLatency !== undefined) { sshData.networkLatency = healthData.networkLatency; } if (healthData.connectionStability !== undefined) { sshData.connectionStability = healthData.connectionStability; } if (healthData.dataTransferRate !== undefined) { sshData.dataTransferRate = healthData.dataTransferRate; } if (healthData.commandExecutionTime !== undefined) { sshData.commandExecutionTime = healthData.commandExecutionTime; } if (healthData.authenticationStatus !== undefined) { sshData.authenticationStatus = healthData.authenticationStatus; } if (healthData.encryptionStrength !== undefined) { sshData.encryptionStrength = healthData.encryptionStrength; } // Update keep-alive success rate if (healthData.keepAliveSuccess !== undefined) { if (healthData.keepAliveSuccess) { sshData.lastKeepAliveSuccess = new Date(); // Update success rate (exponential moving average) sshData.keepAliveSuccessRate = sshData.keepAliveSuccessRate * 0.9 + 0.1; } else { sshData.keepAliveSuccessRate = sshData.keepAliveSuccessRate * 0.9; } } // Update risk factors if (healthData.riskFactors) { Object.assign(sshData.riskFactors, healthData.riskFactors); } // Calculate connection aging const connectionAge = Date.now() - heartbeat.lastBeat.getTime(); const hoursConnected = connectionAge / 3600000; // Connections become riskier after 12 hours if (hoursConnected > 12) { sshData.riskFactors.connectionAging = Math.min( 1.0, (hoursConnected - 12) / 24 ); } // Calculate network degradation based on latency trends if (heartbeat.responseTimeHistory.length > 10) { const recent = heartbeat.responseTimeHistory.slice(-5); const older = heartbeat.responseTimeHistory.slice(-10, -5); const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length; const olderAvg = older.reduce((a, b) => a + b, 0) / older.length; if (recentAvg > olderAvg * 1.5) { sshData.riskFactors.networkDegradation = Math.min( 1.0, (recentAvg - olderAvg) / olderAvg ); } } // Calculate performance issues based on command execution time if (sshData.commandExecutionTime > 5000) { // 5 seconds sshData.riskFactors.performanceIssues = Math.min( 1.0, sshData.commandExecutionTime / 10000 ); } // Authentication risk based on status switch (sshData.authenticationStatus) { case 'failed': sshData.riskFactors.authenticationRisk = 1.0; break; case 'expired': sshData.riskFactors.authenticationRisk = 0.7; break; case 'unknown': sshData.riskFactors.authenticationRisk = 0.3; break; case 'valid': sshData.riskFactors.authenticationRisk = 0.0; break; } } /** * Get comprehensive heartbeat statistics */ getStatistics() { const sessionStats = Array.from(this.sessionHeartbeats.values()); const sshSessions = sessionStats.filter((s) => s.isSSHSession); return { ...this.stats, healthySessions: sessionStats.filter((s) => s.isHealthy).length, averageAdaptiveInterval: sessionStats.length > 0 ? sessionStats.reduce((sum, s) => sum + s.adaptiveInterval, 0) / sessionStats.length : 0, sessionsWithPredictions: sessionStats.filter( (s) => s.failurePredictionScore !== undefined ).length, highRiskSessions: sessionStats.filter( (s) => (s.failurePredictionScore || 0) > 0.7 ).length, isRunning: this.isRunning, configuredInterval: this.config.interval, adaptiveIntervalEnabled: this.config.enableAdaptiveInterval, predictiveFailureEnabled: this.config.enablePredictiveFailure, // SSH-specific statistics sshSessions: { total: sshSessions.length, healthy: sshSessions.filter((s) => s.isHealthy).length, unhealthy: sshSessions.filter((s) => !s.isHealthy).length, highRisk: sshSessions.filter( (s) => s.sshHealthData && this.calculateSSHFailureRisk(s) > this.config.sshFailureRiskThreshold ).length, proactiveReconnectEnabled: this.config.enableSSHProactiveReconnect, averageConnectionStability: sshSessions.length > 0 ? sshSessions .filter((s) => s.sshHealthData) .reduce( (sum, s) => sum + s.sshHealthData!.connectionStability, 0 ) / Math.max(1, sshSessions.filter((s) => s.sshHealthData).length) : 1.0, averageKeepAliveSuccessRate: sshSessions.length > 0 ? sshSessions .filter((s) => s.sshHealthData) .reduce( (sum, s) => sum + s.sshHealthData!.keepAliveSuccessRate, 0 ) / Math.max(1, sshSessions.filter((s) => s.sshHealthData).length) : 1.0, }, }; } /** * Get health summary for all sessions */ getHealthSummary() { const sessions = Array.from(this.sessionHeartbeats.values()); const sshSessions = sessions.filter((s) => s.isSSHSession); return { totalSessions: sessions.length, healthySessions: sessions.filter((s) => s.isHealthy).length, unhealthySessions: sessions.filter((s) => !s.isHealthy).length, averageResponseTime: this.stats.averageResponseTime, successRate: this.stats.totalHeartbeats > 0 ? (this.stats.successfulHeartbeats / this.stats.totalHeartbeats) * 100 : 100, highRiskSessions: sessions.filter( (s) => (s.failurePredictionScore || 0) > 0.7 ).length, lastUpdate: new Date(), // SSH-specific summary ssh: { totalSessions: sshSessions.length, healthySessions: sshSessions.filter((s) => s.isHealthy).length, unhealthySessions: sshSessions.filter((s) => !s.isHealthy).length, highRiskSessions: sshSessions.filter( (s) => s.sshHealthData && this.calculateSSHFailureRisk(s) > this.config.sshFailureRiskThreshold ).length, proactiveReconnectTriggered: sshSessions.filter( (s) => s.sshHealthData?.proactiveReconnectTriggered ).length, averageStability: sshSessions.length > 0 ? sshSessions .filter((s) => s.sshHealthData) .reduce( (sum, s) => sum + s.sshHealthData!.connectionStability, 0 ) / Math.max(1, sshSessions.filter((s) => s.sshHealthData).length) : 1.0, averageNetworkLatency: sshSessions.length > 0 ? sshSessions .filter((s) => s.sshHealthData) .reduce((sum, s) => sum + s.sshHealthData!.networkLatency, 0) / Math.max(1, sshSessions.filter((s) => s.sshHealthData).length) : 0, }, }; } /** * Utility method for delays */ private delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Clean up resources */ async destroy(): Promise<void> { await this.stop(); this.sessionHeartbeats.clear(); this.removeAllListeners(); this.logger.info('HeartbeatMonitor destroyed'); } }

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