MCP GitHub Issue Server
by sammcj
/**
* Storage health monitoring
*/
import { Logger } from '../../logging/index.js';
import { EventManager } from '../../events/event-manager.js';
import { EventTypes } from '../../types/events.js';
export interface HealthMonitorOptions {
checkInterval?: number;
errorThreshold?: number;
responseTimeThreshold?: number;
}
export class HealthMonitor {
private readonly logger: Logger;
private readonly eventManager: EventManager;
private readonly options: Required<HealthMonitorOptions>;
private healthCheckInterval: NodeJS.Timeout | null = null;
private readonly DEFAULT_CHECK_INTERVAL = 30000; // 30 seconds
private readonly DEFAULT_ERROR_THRESHOLD = 5;
private readonly DEFAULT_RESPONSE_TIME_THRESHOLD = 1000; // 1 second
private errorCount = 0;
private lastCheckTime = 0;
private isHealthy = true;
constructor(options: HealthMonitorOptions = {}) {
this.logger = Logger.getInstance().child({ component: 'HealthMonitor' });
this.eventManager = EventManager.getInstance();
this.options = {
checkInterval: options.checkInterval || this.DEFAULT_CHECK_INTERVAL,
errorThreshold: options.errorThreshold || this.DEFAULT_ERROR_THRESHOLD,
responseTimeThreshold: options.responseTimeThreshold || this.DEFAULT_RESPONSE_TIME_THRESHOLD,
};
}
/**
* Start health monitoring
*/
start(): void {
if (this.healthCheckInterval) {
return;
}
this.healthCheckInterval = setInterval(() => {
this.checkHealth();
}, this.options.checkInterval);
// Ensure the interval doesn't prevent the process from exiting
this.healthCheckInterval.unref();
this.logger.info('Health monitoring started', {
interval: this.options.checkInterval,
});
}
/**
* Stop health monitoring
*/
stop(): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
this.logger.info('Health monitoring stopped');
}
/**
* Record error occurrence
*/
recordError(error: Error): void {
this.errorCount++;
this.isHealthy = this.errorCount < this.options.errorThreshold;
if (!this.isHealthy) {
this.logger.error('Health check failed', {
error,
errorCount: this.errorCount,
threshold: this.options.errorThreshold,
});
// Emit health event
this.eventManager.emitSystemEvent({
type: EventTypes.SYSTEM_ERROR,
timestamp: Date.now(),
metadata: {
component: 'HealthMonitor',
error: {
name: error.name,
message: error.message,
...(error.stack && { stack: error.stack }),
...Object.fromEntries(
Object.entries(error).filter(([key]) => typeof (error as any)[key] === 'string')
),
},
memoryUsage: process.memoryUsage(),
},
});
}
}
/**
* Record successful operation
*/
recordSuccess(): void {
// Reset error count on successful operations
if (this.errorCount > 0) {
this.errorCount = 0;
this.isHealthy = true;
}
}
/**
* Get current health status
*/
getHealth(): { isHealthy: boolean; errorCount: number; lastCheck: number } {
return {
isHealthy: this.isHealthy,
errorCount: this.errorCount,
lastCheck: this.lastCheckTime,
};
}
/**
* Reset health status
*/
reset(): void {
this.errorCount = 0;
this.isHealthy = true;
this.lastCheckTime = Date.now();
}
/**
* Perform health check
*/
private checkHealth(): void {
this.lastCheckTime = Date.now();
// Check memory usage
const memoryUsage = process.memoryUsage();
const heapUsedPercent = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
if (heapUsedPercent > 90) {
this.logger.warn('High memory usage detected', {
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
usedPercent: heapUsedPercent,
});
}
// Emit health status event
this.eventManager.emitSystemEvent({
type: EventTypes.SYSTEM_STARTUP,
timestamp: this.lastCheckTime,
metadata: {
component: 'HealthMonitor',
memoryUsage,
success: this.isHealthy,
error: this.isHealthy
? undefined
: {
name: 'HealthCheckError',
message: `Error threshold exceeded: ${this.errorCount}`,
},
},
});
// Log health status
this.logger.info('Health check completed', {
isHealthy: this.isHealthy,
errorCount: this.errorCount,
memoryUsage: {
heapUsedPercent,
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB',
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) + 'MB',
},
});
}
}