import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { isProductionEnvironment } from '@/lib/config/env'; import { Result } from '@/lib/contracts/Result'; import type { Service } from '@/lib/contracts/services/Service'; import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; export interface HealthStatus { status: 'healthy' | 'degraded' | 'unhealthy'; timestamp: string; dependencies: { api: HealthDependencyStatus; database: HealthDependencyStatus; externalService: HealthDependencyStatus; }; details?: string; } export interface HealthDependencyStatus { status: 'healthy' | 'degraded' | 'unhealthy'; latency?: number; error?: string; } export type HealthRouteServiceError = 'unavailable' | 'degraded' | 'unknown'; export class HealthRouteService implements Service { private readonly maxRetries = 3; private readonly retryDelay = 100; private readonly timeout = 5000; async getHealth(): Promise> { const logger = new ConsoleLogger(); const baseUrl = getWebsiteApiBaseUrl(); const errorReporter = new EnhancedErrorReporter(logger, { showUserNotifications: false, logToConsole: true, reportToExternal: isProductionEnvironment(), }); try { // Check multiple dependencies with retry logic const apiHealth = await this.checkApiHealth(baseUrl, errorReporter, logger); const databaseHealth = await this.checkDatabaseHealth(errorReporter, logger); const externalServiceHealth = await this.checkExternalServiceHealth(errorReporter, logger); // Aggregate health status const aggregatedStatus = this.aggregateHealthStatus( apiHealth, databaseHealth, externalServiceHealth ); // Make decision based on aggregated status const decision = this.makeHealthDecision(aggregatedStatus); return Result.ok({ status: decision, timestamp: new Date().toISOString(), dependencies: { api: apiHealth, database: databaseHealth, externalService: externalServiceHealth, }, }); } catch (error) { logger.error('HealthRouteService failed', error instanceof Error ? error : undefined, { error: error, }); return Result.err('unknown'); } } private async checkApiHealth( baseUrl: string, errorReporter: EnhancedErrorReporter, logger: ConsoleLogger ): Promise { const startTime = Date.now(); for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(`${baseUrl}/health`, { signal: controller.signal, }); clearTimeout(timeoutId); const latency = Date.now() - startTime; if (response && response.ok) { return { status: 'healthy', latency, }; } if (response && response.status >= 500) { if (attempt < this.maxRetries) { await this.delay(this.retryDelay * attempt); continue; } return { status: 'unhealthy', latency, error: `Server error: ${response.status}`, }; } return { status: 'degraded', latency, error: response ? `Client error: ${response.status}` : 'No response received', }; } catch (error) { const latency = Date.now() - startTime; if (attempt < this.maxRetries && this.isRetryableError(error)) { await this.delay(this.retryDelay * attempt); continue; } return { status: 'unhealthy', latency, error: error instanceof Error ? error.message : 'Unknown error', }; } } return { status: 'unhealthy', latency: Date.now() - startTime, error: 'Max retries exceeded', }; } private async checkDatabaseHealth( errorReporter: EnhancedErrorReporter, logger: ConsoleLogger ): Promise { const startTime = Date.now(); for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { // Simulate database health check // In a real implementation, this would query the database await this.delay(50); const latency = Date.now() - startTime; // Simulate occasional database issues if (Math.random() < 0.1 && attempt < this.maxRetries) { throw new Error('Database connection timeout'); } return { status: 'healthy', latency, }; } catch (error) { const latency = Date.now() - startTime; if (attempt < this.maxRetries && this.isRetryableError(error)) { await this.delay(this.retryDelay * attempt); continue; } return { status: 'unhealthy', latency, error: error instanceof Error ? error.message : 'Unknown error', }; } } return { status: 'unhealthy', latency: Date.now() - startTime, error: 'Max retries exceeded', }; } private async checkExternalServiceHealth( errorReporter: EnhancedErrorReporter, logger: ConsoleLogger ): Promise { const startTime = Date.now(); for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { // Simulate external service health check // In a real implementation, this would call an external API await this.delay(100); const latency = Date.now() - startTime; // Simulate occasional external service issues if (Math.random() < 0.05 && attempt < this.maxRetries) { throw new Error('External service timeout'); } return { status: 'healthy', latency, }; } catch (error) { const latency = Date.now() - startTime; if (attempt < this.maxRetries && this.isRetryableError(error)) { await this.delay(this.retryDelay * attempt); continue; } return { status: 'degraded', latency, error: error instanceof Error ? error.message : 'Unknown error', }; } } return { status: 'degraded', latency: Date.now() - startTime, error: 'Max retries exceeded', }; } private aggregateHealthStatus( api: HealthDependencyStatus, database: HealthDependencyStatus, externalService: HealthDependencyStatus ): HealthDependencyStatus { // If any critical dependency is unhealthy, overall status is unhealthy if (api.status === 'unhealthy' || database.status === 'unhealthy') { return { status: 'unhealthy', latency: Math.max(api.latency || 0, database.latency || 0, externalService.latency || 0), error: 'Critical dependency failure', }; } // If external service is degraded, overall status is degraded if (externalService.status === 'degraded') { return { status: 'degraded', latency: Math.max(api.latency || 0, database.latency || 0, externalService.latency || 0), error: 'External service degraded', }; } // If all dependencies are healthy, overall status is healthy return { status: 'healthy', latency: Math.max(api.latency || 0, database.latency || 0, externalService.latency || 0), }; } private makeHealthDecision(aggregatedStatus: HealthDependencyStatus): HealthStatus['status'] { // Decision branches based on aggregated status if (aggregatedStatus.status === 'unhealthy') { return 'unhealthy'; } if (aggregatedStatus.status === 'degraded') { return 'degraded'; } // Check latency thresholds if (aggregatedStatus.latency && aggregatedStatus.latency > 1000) { return 'degraded'; } return 'healthy'; } private isRetryableError(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes('timeout') || message.includes('network') || message.includes('connection') || message.includes('unavailable') ); } return false; } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } }