Files
gridpilot.gg/apps/website/lib/services/health/HealthRouteService.ts
Marc Mintel 6c07abe5e7
Some checks failed
Contract Testing / contract-snapshot (pull_request) Has been cancelled
Contract Testing / contract-tests (pull_request) Has been cancelled
view data fixes
2026-01-25 00:12:30 +01:00

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));
}
}