port-nimara-client-portal/server/utils/health-checker.ts

145 lines
4.0 KiB
TypeScript

import type { H3Event } from 'h3';
interface ServiceCheckResult {
status: 'up' | 'down' | 'slow';
responseTime?: number;
error?: string;
}
class HealthChecker {
private static instance: HealthChecker;
private isReady: boolean = false;
private startupChecks: Map<string, boolean> = new Map();
private requiredServices = ['nocodb', 'directus', 'minio'];
private constructor() {
this.initializeStartupChecks();
}
static getInstance(): HealthChecker {
if (!HealthChecker.instance) {
HealthChecker.instance = new HealthChecker();
}
return HealthChecker.instance;
}
private initializeStartupChecks() {
this.requiredServices.forEach(service => {
this.startupChecks.set(service, false);
});
}
async checkServiceWithRetry(
serviceName: string,
checkFunction: () => Promise<ServiceCheckResult>,
maxRetries: number = 3,
retryDelay: number = 1000
): Promise<ServiceCheckResult> {
let lastError: string | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const result = await checkFunction();
if (result.status === 'up' || result.status === 'slow') {
this.startupChecks.set(serviceName, true);
return result;
}
lastError = result.error;
} catch (error) {
lastError = error instanceof Error ? error.message : 'Unknown error';
}
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
}
}
return {
status: 'down',
error: lastError || 'Max retries exceeded'
};
}
markServiceReady(serviceName: string) {
this.startupChecks.set(serviceName, true);
this.checkOverallReadiness();
}
markServiceDown(serviceName: string) {
this.startupChecks.set(serviceName, false);
this.isReady = false;
}
private checkOverallReadiness() {
const allRequired = this.requiredServices.every(service =>
this.startupChecks.get(service) === true
);
if (allRequired) {
this.isReady = true;
console.log('[HealthChecker] All required services are ready');
}
}
isApplicationReady(): boolean {
return this.isReady;
}
getStartupStatus(): { [key: string]: boolean } {
const status: { [key: string]: boolean } = {};
this.startupChecks.forEach((value, key) => {
status[key] = value;
});
return status;
}
async performStartupHealthChecks(): Promise<boolean> {
console.log('[HealthChecker] Performing startup health checks...');
// Import check functions dynamically to avoid circular dependencies
const { checkNocoDB, checkDirectus, checkMinIO } = await import('./service-checks');
// Check all services in parallel
const checks = await Promise.all([
this.checkServiceWithRetry('nocodb', checkNocoDB, 5, 2000),
this.checkServiceWithRetry('directus', checkDirectus, 5, 2000),
this.checkServiceWithRetry('minio', checkMinIO, 5, 2000)
]);
// Log results
checks.forEach((result, index) => {
const serviceName = ['nocodb', 'directus', 'minio'][index];
console.log(`[HealthChecker] ${serviceName}: ${result.status}${result.error ? ` - ${result.error}` : ''}`);
});
this.checkOverallReadiness();
return this.isReady;
}
}
export const healthChecker = HealthChecker.getInstance();
// Middleware to check if app is ready
export function createReadinessMiddleware() {
return defineEventHandler(async (event: H3Event) => {
// Skip health check endpoint itself
if (event.node.req.url === '/api/health') {
return;
}
// During startup, return 503 for all requests except health
if (!healthChecker.isApplicationReady()) {
const startupStatus = healthChecker.getStartupStatus();
setResponseStatus(event, 503);
return {
error: 'Service Unavailable',
message: 'Application is starting up. Please wait...',
startupStatus
};
}
});
}