UNPKG

@knath2000/codebase-indexing-mcp

Version:

MCP server for codebase indexing with Voyage AI embeddings and Qdrant vector storage

307 lines (270 loc) 8.49 kB
import { HealthStatus, ServiceHealth, Config } from '../types.js'; import { VoyageClient } from '../clients/voyage-client.js'; import { QdrantVectorClient } from '../clients/qdrant-client.js'; export class HealthMonitorService { private config: Config; private startTime: Date; private voyageClient: VoyageClient; private qdrantClient: QdrantVectorClient; private lastHealthCheck: Date; private healthCheckInterval: NodeJS.Timeout | null = null; constructor(config: Config, voyageClient: VoyageClient, qdrantClient: QdrantVectorClient) { this.config = config; this.startTime = new Date(); this.voyageClient = voyageClient; this.qdrantClient = qdrantClient; this.lastHealthCheck = new Date(); // Start periodic health checks this.startHealthChecks(); } /** * Get comprehensive health status */ async getHealthStatus(): Promise<HealthStatus> { console.log('🏥 [HealthMonitor] Performing health check...'); const [qdrantHealth, voyageHealth, fileWatcherHealth] = await Promise.allSettled([ this.checkQdrantHealth(), this.checkVoyageHealth(), this.checkFileWatcherHealth() ]); const services = { qdrant: qdrantHealth.status === 'fulfilled' ? qdrantHealth.value : this.getFailedServiceHealth('Qdrant check failed'), voyage: voyageHealth.status === 'fulfilled' ? voyageHealth.value : this.getFailedServiceHealth('Voyage check failed'), fileWatcher: fileWatcherHealth.status === 'fulfilled' ? fileWatcherHealth.value : this.getFailedServiceHealth('File watcher check failed') }; // Determine overall status const serviceStatuses = Object.values(services).map(s => s.status); let overallStatus: 'healthy' | 'degraded' | 'unhealthy'; if (serviceStatuses.every(s => s === 'healthy')) { overallStatus = 'healthy'; } else if (serviceStatuses.some(s => s === 'unhealthy')) { overallStatus = 'unhealthy'; } else { overallStatus = 'degraded'; } const metrics = await this.getSystemMetrics(); const healthStatus: HealthStatus = { status: overallStatus, timestamp: new Date(), services, metrics, version: '1.0.0', // TODO: Get from package.json mcpSchemaVersion: this.config.mcpSchemaVersion }; this.lastHealthCheck = new Date(); console.log(`✅ [HealthMonitor] Health check complete: ${overallStatus}`); return healthStatus; } /** * Check Qdrant service health */ private async checkQdrantHealth(): Promise<ServiceHealth> { const startTime = Date.now(); try { const isConnected = await this.qdrantClient.testConnection(); const latency = Date.now() - startTime; if (!isConnected) { return { status: 'unhealthy', latency, lastCheck: new Date(), message: 'Qdrant connection test failed' }; } // Additional health checks try { await this.qdrantClient.getCollectionInfo(); return { status: 'healthy', latency, lastCheck: new Date(), message: 'Qdrant operational' }; } catch (collectionError) { return { status: 'degraded', latency, lastCheck: new Date(), message: 'Qdrant connected but collection not found' }; } } catch (error) { return { status: 'unhealthy', latency: Date.now() - startTime, lastCheck: new Date(), message: `Qdrant error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Check Voyage AI service health */ private async checkVoyageHealth(): Promise<ServiceHealth> { const startTime = Date.now(); try { const isConnected = await this.voyageClient.testConnection(); const latency = Date.now() - startTime; return { status: isConnected ? 'healthy' : 'unhealthy', latency, lastCheck: new Date(), message: isConnected ? 'Voyage AI operational' : 'Voyage AI connection failed' }; } catch (error) { return { status: 'unhealthy', latency: Date.now() - startTime, lastCheck: new Date(), message: `Voyage AI error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Check file watcher service health */ private async checkFileWatcherHealth(): Promise<ServiceHealth> { // For now, assume file watcher is healthy if the process is running // In a real implementation, you'd check the actual watcher status return { status: 'healthy', latency: 0, lastCheck: new Date(), message: 'File watcher operational' }; } /** * Get system metrics */ private async getSystemMetrics(): Promise<{ uptime: number; memoryUsage: number; cpuUsage?: number; diskUsage?: number; }> { const uptime = Date.now() - this.startTime.getTime(); // Get memory usage const memoryUsage = process.memoryUsage(); const memoryUsageMB = Math.round(memoryUsage.heapUsed / 1024 / 1024); return { uptime: Math.round(uptime / 1000), // Convert to seconds memoryUsage: memoryUsageMB // CPU and disk usage would require additional libraries }; } /** * Create a failed service health object */ private getFailedServiceHealth(message: string): ServiceHealth { return { status: 'unhealthy', lastCheck: new Date(), message }; } /** * Start periodic health checks */ private startHealthChecks(): void { // Run health checks every 5 minutes this.healthCheckInterval = setInterval(async () => { try { await this.getHealthStatus(); } catch (error) { console.error('❌ [HealthMonitor] Periodic health check failed:', error); } }, 5 * 60 * 1000); console.log('🏥 [HealthMonitor] Started periodic health checks (5 min intervals)'); } /** * Stop periodic health checks */ stopHealthChecks(): void { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = null; console.log('🏥 [HealthMonitor] Stopped periodic health checks'); } } /** * Get simple health status for quick checks */ async getSimpleHealth(): Promise<{ status: string; timestamp: Date }> { try { const health = await this.getHealthStatus(); return { status: health.status, timestamp: health.timestamp }; } catch (error) { return { status: 'unhealthy', timestamp: new Date() }; } } /** * Check if the service is ready to serve requests */ async isReady(): Promise<boolean> { try { const health = await this.getHealthStatus(); return health.status !== 'unhealthy'; } catch (error) { return false; } } /** * Check if the service is alive (basic liveness check) */ isAlive(): boolean { // Basic check - if we can execute this function, the service is alive return true; } /** * Get detailed service statistics */ async getDetailedStats(): Promise<{ health: HealthStatus; performance: { averageResponseTime: number; requestCount: number; errorRate: number; }; resources: { memoryUsage: number; uptime: number; lastHealthCheck: Date; }; }> { const health = await this.getHealthStatus(); return { health, performance: { averageResponseTime: 0, // TODO: Implement performance tracking requestCount: 0, // TODO: Implement request counting errorRate: 0 // TODO: Implement error rate tracking }, resources: { memoryUsage: health.metrics.memoryUsage, uptime: health.metrics.uptime, lastHealthCheck: this.lastHealthCheck } }; } /** * Get health summary for logging/monitoring */ getHealthSummary(): string { const uptime = Math.round((Date.now() - this.startTime.getTime()) / 1000); const memory = Math.round(process.memoryUsage().heapUsed / 1024 / 1024); return `Uptime: ${uptime}s, Memory: ${memory}MB, Last Check: ${this.lastHealthCheck.toISOString()}`; } /** * Cleanup resources */ destroy(): void { this.stopHealthChecks(); console.log('🏥 [HealthMonitor] Destroyed health monitor'); } }