145 lines
4.0 KiB
TypeScript
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
|
|
};
|
|
}
|
|
});
|
|
}
|