226 lines
6.4 KiB
TypeScript
226 lines
6.4 KiB
TypeScript
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<MongoDBHealthCheck> {
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|