145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
import { getLogger } from '@stock-bot/logger';
|
|
import type { PostgreSQLClient } from './client';
|
|
import type { PostgreSQLHealthCheck, PostgreSQLHealthStatus, PostgreSQLMetrics } from './types';
|
|
|
|
/**
|
|
* PostgreSQL Health Monitor
|
|
*
|
|
* Monitors PostgreSQL connection health and provides metrics
|
|
*/
|
|
export class PostgreSQLHealthMonitor {
|
|
private readonly client: PostgreSQLClient;
|
|
private readonly logger: ReturnType<typeof getLogger>;
|
|
private healthCheckInterval: NodeJS.Timeout | null = null;
|
|
private metrics: PostgreSQLMetrics;
|
|
private lastHealthCheck: PostgreSQLHealthCheck | null = null;
|
|
|
|
constructor(client: PostgreSQLClient) {
|
|
this.client = client;
|
|
this.logger = getLogger('postgres-health-monitor');
|
|
this.metrics = {
|
|
queriesPerSecond: 0,
|
|
averageQueryTime: 0,
|
|
errorRate: 0,
|
|
connectionPoolUtilization: 0,
|
|
slowQueries: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Start health monitoring
|
|
*/
|
|
start(intervalMs: number = 30000): void {
|
|
if (this.healthCheckInterval) {
|
|
this.stop();
|
|
}
|
|
|
|
this.logger.info(`Starting PostgreSQL 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 PostgreSQL health monitoring');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current health status
|
|
*/
|
|
async getHealth(): Promise<PostgreSQLHealthCheck> {
|
|
if (!this.lastHealthCheck) {
|
|
await this.performHealthCheck();
|
|
}
|
|
if (!this.lastHealthCheck) {
|
|
throw new Error('Health check failed to produce results');
|
|
}
|
|
return this.lastHealthCheck;
|
|
}
|
|
|
|
/**
|
|
* Get current metrics
|
|
*/
|
|
getMetrics(): PostgreSQLMetrics {
|
|
return { ...this.metrics };
|
|
}
|
|
|
|
/**
|
|
* Perform a health check
|
|
*/
|
|
private async performHealthCheck(): Promise<void> {
|
|
const startTime = Date.now();
|
|
const errors: string[] = [];
|
|
let status: PostgreSQLHealthStatus = 'healthy';
|
|
|
|
try {
|
|
if (!this.client.connected) {
|
|
errors.push('PostgreSQL client not connected');
|
|
status = 'unhealthy';
|
|
} else {
|
|
// Test basic connectivity
|
|
await this.client.query('SELECT 1');
|
|
|
|
// Get connection stats
|
|
const stats = await this.client.getStats();
|
|
|
|
// Check connection pool utilization
|
|
const utilization = parseInt(stats.active_connections) / parseInt(stats.max_connections);
|
|
if (utilization > 0.8) {
|
|
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;
|
|
}
|
|
|
|
this.metrics.connectionPoolUtilization = utilization;
|
|
}
|
|
} catch (error) {
|
|
errors.push(`Health check failed: ${(error as Error).message}`);
|
|
status = 'unhealthy';
|
|
}
|
|
|
|
const latency = Date.now() - startTime;
|
|
|
|
this.lastHealthCheck = {
|
|
status,
|
|
timestamp: new Date(),
|
|
latency,
|
|
connections: {
|
|
active: 1,
|
|
idle: 9,
|
|
total: 10,
|
|
},
|
|
errors: errors.length > 0 ? errors : undefined,
|
|
};
|
|
|
|
// Log health status changes
|
|
if (status !== 'healthy') {
|
|
this.logger.warn(`PostgreSQL health status: ${status}`, { errors, latency });
|
|
} else {
|
|
this.logger.debug(`PostgreSQL health check passed (${latency}ms)`);
|
|
}
|
|
}
|
|
}
|