299 lines
8.5 KiB
TypeScript
299 lines
8.5 KiB
TypeScript
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<Result<HealthStatus, HealthRouteServiceError>> {
|
|
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<HealthDependencyStatus> {
|
|
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<HealthDependencyStatus> {
|
|
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<HealthDependencyStatus> {
|
|
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<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
}
|