import { Logger } from '@stock-bot/logger'; import type { MongoDBClient } from './client'; import type { MongoDBHealthCheck, MongoDBHealthStatus, MongoDBMetrics } from './types'; /** * MongoDB Health Monitor * * Monitors MongoDB connection health and provides metrics */ export class MongoDBHealthMonitor { private readonly client: MongoDBClient; private readonly logger: Logger; private healthCheckInterval: NodeJS.Timeout | null = null; private metrics: MongoDBMetrics; private lastHealthCheck: MongoDBHealthCheck | null = null; constructor(client: MongoDBClient) { this.client = client; this.logger = new Logger('MongoDBHealthMonitor'); this.metrics = { operationsPerSecond: 0, averageLatency: 0, errorRate: 0, connectionPoolUtilization: 0, documentsProcessed: 0 }; } /** * Start health monitoring */ start(intervalMs: number = 30000): void { if (this.healthCheckInterval) { this.stop(); } this.logger.info(`Starting MongoDB health monitoring (interval: ${intervalMs}ms)`); this.healthCheckInterval = setInterval(async () => { try { await this.performHealthCheck(); } catch (error) { this.logger.error('Health check failed:', error); } }, intervalMs); // Perform initial health check this.performHealthCheck().catch(error => { this.logger.error('Initial health check failed:', error); }); } /** * Stop health monitoring */ stop(): void { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = null; this.logger.info('Stopped MongoDB health monitoring'); } } /** * Get current health status */ async getHealth(): Promise { if (!this.lastHealthCheck) { await this.performHealthCheck(); } return this.lastHealthCheck!; } /** * Get current metrics */ getMetrics(): MongoDBMetrics { return { ...this.metrics }; } /** * Perform a health check */ private async performHealthCheck(): Promise { const startTime = Date.now(); const errors: string[] = []; let status: MongoDBHealthStatus = 'healthy'; try { if (!this.client.connected) { errors.push('MongoDB client not connected'); status = 'unhealthy'; } else { // Test basic connectivity const mongoClient = this.client.mongoClient; const db = this.client.database; if (!mongoClient || !db) { errors.push('MongoDB client or database not available'); status = 'unhealthy'; } else { // Ping the database await db.admin().ping(); // Get server status for metrics try { const serverStatus = await db.admin().serverStatus(); this.updateMetricsFromServerStatus(serverStatus); // Check connection pool status const poolStats = this.getConnectionPoolStats(serverStatus); if (poolStats.utilization > 0.9) { errors.push('High connection pool utilization'); status = status === 'healthy' ? 'degraded' : status; } // Check for high latency const latency = Date.now() - startTime; if (latency > 1000) { errors.push(`High latency: ${latency}ms`); status = status === 'healthy' ? 'degraded' : status; } } catch (statusError) { errors.push(`Failed to get server status: ${(statusError as Error).message}`); status = 'degraded'; } } } } catch (error) { errors.push(`Health check failed: ${(error as Error).message}`); status = 'unhealthy'; } const latency = Date.now() - startTime; // Get connection stats const connectionStats = this.getConnectionStats(); this.lastHealthCheck = { status, timestamp: new Date(), latency, connections: connectionStats, errors: errors.length > 0 ? errors : undefined }; // Log health status changes if (status !== 'healthy') { this.logger.warn(`MongoDB health status: ${status}`, { errors, latency }); } else { this.logger.debug(`MongoDB health check passed (${latency}ms)`); } } /** * Update metrics from MongoDB server status */ private updateMetricsFromServerStatus(serverStatus: any): void { try { const opcounters = serverStatus.opcounters || {}; const connections = serverStatus.connections || {}; const dur = serverStatus.dur || {}; // Calculate operations per second (approximate) const totalOps = Object.values(opcounters).reduce((sum: number, count: any) => sum + (count || 0), 0); this.metrics.operationsPerSecond = totalOps; // Connection pool utilization if (connections.current && connections.available) { const total = connections.current + connections.available; this.metrics.connectionPoolUtilization = connections.current / total; } // Average latency (from durability stats if available) if (dur.timeMS) { this.metrics.averageLatency = dur.timeMS.dt || 0; } } catch (error) { this.logger.debug('Error parsing server status for metrics:', error as any); } } /** * Get connection pool statistics */ private getConnectionPoolStats(serverStatus: any): { utilization: number; active: number; available: number } { const connections = serverStatus.connections || {}; const active = connections.current || 0; const available = connections.available || 0; const total = active + available; return { utilization: total > 0 ? active / total : 0, active, available }; } /** * Get connection statistics */ private getConnectionStats(): { active: number; available: number; total: number } { // This would ideally come from the MongoDB driver's connection pool // For now, we'll return estimated values return { active: 1, available: 9, total: 10 }; } /** * Update error rate metric */ updateErrorRate(errorCount: number, totalOperations: number): void { this.metrics.errorRate = totalOperations > 0 ? errorCount / totalOperations : 0; } /** * Update documents processed metric */ updateDocumentsProcessed(count: number): void { this.metrics.documentsProcessed += count; } }