add tests
This commit is contained in:
112
apps/website/lib/builders/view-data/HealthViewDataBuilder.ts
Normal file
112
apps/website/lib/builders/view-data/HealthViewDataBuilder.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Health View Data Builder
|
||||
*
|
||||
* Transforms health DTO data into UI-ready view models.
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*/
|
||||
|
||||
import type { HealthViewData, HealthStatus, HealthMetrics, HealthComponent, HealthAlert } from '@/lib/view-data/HealthViewData';
|
||||
import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay';
|
||||
import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay';
|
||||
import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay';
|
||||
import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay';
|
||||
|
||||
export interface HealthDTO {
|
||||
status: 'ok' | 'degraded' | 'error' | 'unknown';
|
||||
timestamp: string;
|
||||
uptime?: number;
|
||||
responseTime?: number;
|
||||
errorRate?: number;
|
||||
lastCheck?: string;
|
||||
checksPassed?: number;
|
||||
checksFailed?: number;
|
||||
components?: Array<{
|
||||
name: string;
|
||||
status: 'ok' | 'degraded' | 'error' | 'unknown';
|
||||
lastCheck?: string;
|
||||
responseTime?: number;
|
||||
errorRate?: number;
|
||||
}>;
|
||||
alerts?: Array<{
|
||||
id: string;
|
||||
type: 'critical' | 'warning' | 'info';
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class HealthViewDataBuilder {
|
||||
static build(dto: HealthDTO): HealthViewData {
|
||||
const now = new Date();
|
||||
const lastUpdated = dto.timestamp || now.toISOString();
|
||||
|
||||
// Build overall status
|
||||
const overallStatus: HealthStatus = {
|
||||
status: dto.status,
|
||||
timestamp: dto.timestamp,
|
||||
formattedTimestamp: HealthStatusDisplay.formatTimestamp(dto.timestamp),
|
||||
relativeTime: HealthStatusDisplay.formatRelativeTime(dto.timestamp),
|
||||
statusLabel: HealthStatusDisplay.formatStatusLabel(dto.status),
|
||||
statusColor: HealthStatusDisplay.formatStatusColor(dto.status),
|
||||
statusIcon: HealthStatusDisplay.formatStatusIcon(dto.status),
|
||||
};
|
||||
|
||||
// Build metrics
|
||||
const metrics: HealthMetrics = {
|
||||
uptime: HealthMetricDisplay.formatUptime(dto.uptime),
|
||||
responseTime: HealthMetricDisplay.formatResponseTime(dto.responseTime),
|
||||
errorRate: HealthMetricDisplay.formatErrorRate(dto.errorRate),
|
||||
lastCheck: dto.lastCheck || lastUpdated,
|
||||
formattedLastCheck: HealthMetricDisplay.formatTimestamp(dto.lastCheck || lastUpdated),
|
||||
checksPassed: dto.checksPassed || 0,
|
||||
checksFailed: dto.checksFailed || 0,
|
||||
totalChecks: (dto.checksPassed || 0) + (dto.checksFailed || 0),
|
||||
successRate: HealthMetricDisplay.formatSuccessRate(dto.checksPassed, dto.checksFailed),
|
||||
};
|
||||
|
||||
// Build components
|
||||
const components: HealthComponent[] = (dto.components || []).map((component) => ({
|
||||
name: component.name,
|
||||
status: component.status,
|
||||
statusLabel: HealthComponentDisplay.formatStatusLabel(component.status),
|
||||
statusColor: HealthComponentDisplay.formatStatusColor(component.status),
|
||||
statusIcon: HealthComponentDisplay.formatStatusIcon(component.status),
|
||||
lastCheck: component.lastCheck || lastUpdated,
|
||||
formattedLastCheck: HealthComponentDisplay.formatTimestamp(component.lastCheck || lastUpdated),
|
||||
responseTime: HealthMetricDisplay.formatResponseTime(component.responseTime),
|
||||
errorRate: HealthMetricDisplay.formatErrorRate(component.errorRate),
|
||||
}));
|
||||
|
||||
// Build alerts
|
||||
const alerts: HealthAlert[] = (dto.alerts || []).map((alert) => ({
|
||||
id: alert.id,
|
||||
type: alert.type,
|
||||
title: alert.title,
|
||||
message: alert.message,
|
||||
timestamp: alert.timestamp,
|
||||
formattedTimestamp: HealthAlertDisplay.formatTimestamp(alert.timestamp),
|
||||
relativeTime: HealthAlertDisplay.formatRelativeTime(alert.timestamp),
|
||||
severity: HealthAlertDisplay.formatSeverity(alert.type),
|
||||
severityColor: HealthAlertDisplay.formatSeverityColor(alert.type),
|
||||
}));
|
||||
|
||||
// Calculate derived fields
|
||||
const hasAlerts = alerts.length > 0;
|
||||
const hasDegradedComponents = components.some((c) => c.status === 'degraded');
|
||||
const hasErrorComponents = components.some((c) => c.status === 'error');
|
||||
|
||||
return {
|
||||
overallStatus,
|
||||
metrics,
|
||||
components,
|
||||
alerts,
|
||||
hasAlerts,
|
||||
hasDegradedComponents,
|
||||
hasErrorComponents,
|
||||
lastUpdated,
|
||||
formattedLastUpdated: HealthStatusDisplay.formatTimestamp(lastUpdated),
|
||||
};
|
||||
}
|
||||
}
|
||||
53
apps/website/lib/display-objects/HealthAlertDisplay.ts
Normal file
53
apps/website/lib/display-objects/HealthAlertDisplay.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Health Alert Display Object
|
||||
*
|
||||
* Provides formatting and display logic for health alerts.
|
||||
* This display object isolates UI-specific formatting from business logic.
|
||||
*/
|
||||
|
||||
export class HealthAlertDisplay {
|
||||
static formatSeverity(type: 'critical' | 'warning' | 'info'): string {
|
||||
const severities: Record<string, string> = {
|
||||
critical: 'Critical',
|
||||
warning: 'Warning',
|
||||
info: 'Info',
|
||||
};
|
||||
return severities[type] || 'Info';
|
||||
}
|
||||
|
||||
static formatSeverityColor(type: 'critical' | 'warning' | 'info'): string {
|
||||
const colors: Record<string, string> = {
|
||||
critical: '#ef4444', // red-500
|
||||
warning: '#f59e0b', // amber-500
|
||||
info: '#3b82f6', // blue-500
|
||||
};
|
||||
return colors[type] || '#3b82f6';
|
||||
}
|
||||
|
||||
static formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
static formatRelativeTime(timestamp: string): string {
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return `${Math.floor(diffDays / 7)}w ago`;
|
||||
}
|
||||
}
|
||||
50
apps/website/lib/display-objects/HealthComponentDisplay.ts
Normal file
50
apps/website/lib/display-objects/HealthComponentDisplay.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Health Component Display Object
|
||||
*
|
||||
* Provides formatting and display logic for health components.
|
||||
* This display object isolates UI-specific formatting from business logic.
|
||||
*/
|
||||
|
||||
export class HealthComponentDisplay {
|
||||
static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const labels: Record<string, string> = {
|
||||
ok: 'Healthy',
|
||||
degraded: 'Degraded',
|
||||
error: 'Error',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
return labels[status] || 'Unknown';
|
||||
}
|
||||
|
||||
static formatStatusColor(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const colors: Record<string, string> = {
|
||||
ok: '#10b981', // green-500
|
||||
degraded: '#f59e0b', // amber-500
|
||||
error: '#ef4444', // red-500
|
||||
unknown: '#6b7280', // gray-500
|
||||
};
|
||||
return colors[status] || '#6b7280';
|
||||
}
|
||||
|
||||
static formatStatusIcon(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const icons: Record<string, string> = {
|
||||
ok: '✓',
|
||||
degraded: '⚠',
|
||||
error: '✕',
|
||||
unknown: '?',
|
||||
};
|
||||
return icons[status] || '?';
|
||||
}
|
||||
|
||||
static formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
||||
61
apps/website/lib/display-objects/HealthMetricDisplay.ts
Normal file
61
apps/website/lib/display-objects/HealthMetricDisplay.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Health Metric Display Object
|
||||
*
|
||||
* Provides formatting and display logic for health metrics.
|
||||
* This display object isolates UI-specific formatting from business logic.
|
||||
*/
|
||||
|
||||
export class HealthMetricDisplay {
|
||||
static formatUptime(uptime?: number): string {
|
||||
if (uptime === undefined || uptime === null) return 'N/A';
|
||||
if (uptime < 0) return 'N/A';
|
||||
|
||||
// Format as percentage with 2 decimal places
|
||||
return `${uptime.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
static formatResponseTime(responseTime?: number): string {
|
||||
if (responseTime === undefined || responseTime === null) return 'N/A';
|
||||
if (responseTime < 0) return 'N/A';
|
||||
|
||||
// Format as milliseconds with appropriate units
|
||||
if (responseTime < 1000) {
|
||||
return `${responseTime.toFixed(0)}ms`;
|
||||
} else if (responseTime < 60000) {
|
||||
return `${(responseTime / 1000).toFixed(2)}s`;
|
||||
} else {
|
||||
return `${(responseTime / 60000).toFixed(2)}m`;
|
||||
}
|
||||
}
|
||||
|
||||
static formatErrorRate(errorRate?: number): string {
|
||||
if (errorRate === undefined || errorRate === null) return 'N/A';
|
||||
if (errorRate < 0) return 'N/A';
|
||||
|
||||
// Format as percentage with 2 decimal places
|
||||
return `${errorRate.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
static formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
static formatSuccessRate(checksPassed?: number, checksFailed?: number): string {
|
||||
const passed = checksPassed || 0;
|
||||
const failed = checksFailed || 0;
|
||||
const total = passed + failed;
|
||||
|
||||
if (total === 0) return 'N/A';
|
||||
|
||||
const successRate = (passed / total) * 100;
|
||||
return `${successRate.toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
65
apps/website/lib/display-objects/HealthStatusDisplay.ts
Normal file
65
apps/website/lib/display-objects/HealthStatusDisplay.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Health Status Display Object
|
||||
*
|
||||
* Provides formatting and display logic for health status data.
|
||||
* This display object isolates UI-specific formatting from business logic.
|
||||
*/
|
||||
|
||||
export class HealthStatusDisplay {
|
||||
static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const labels: Record<string, string> = {
|
||||
ok: 'Healthy',
|
||||
degraded: 'Degraded',
|
||||
error: 'Error',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
return labels[status] || 'Unknown';
|
||||
}
|
||||
|
||||
static formatStatusColor(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const colors: Record<string, string> = {
|
||||
ok: '#10b981', // green-500
|
||||
degraded: '#f59e0b', // amber-500
|
||||
error: '#ef4444', // red-500
|
||||
unknown: '#6b7280', // gray-500
|
||||
};
|
||||
return colors[status] || '#6b7280';
|
||||
}
|
||||
|
||||
static formatStatusIcon(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
|
||||
const icons: Record<string, string> = {
|
||||
ok: '✓',
|
||||
degraded: '⚠',
|
||||
error: '✕',
|
||||
unknown: '?',
|
||||
};
|
||||
return icons[status] || '?';
|
||||
}
|
||||
|
||||
static formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
static formatRelativeTime(timestamp: string): string {
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return `${Math.floor(diffDays / 7)}w ago`;
|
||||
}
|
||||
}
|
||||
298
apps/website/lib/services/health/HealthRouteService.ts
Normal file
298
apps/website/lib/services/health/HealthRouteService.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
65
apps/website/lib/view-data/HealthViewData.ts
Normal file
65
apps/website/lib/view-data/HealthViewData.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Health View Data Types
|
||||
*
|
||||
* Defines the UI model for health monitoring data.
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*/
|
||||
|
||||
export interface HealthStatus {
|
||||
status: 'ok' | 'degraded' | 'error' | 'unknown';
|
||||
timestamp: string;
|
||||
formattedTimestamp: string;
|
||||
relativeTime: string;
|
||||
statusLabel: string;
|
||||
statusColor: string;
|
||||
statusIcon: string;
|
||||
}
|
||||
|
||||
export interface HealthMetrics {
|
||||
uptime: string;
|
||||
responseTime: string;
|
||||
errorRate: string;
|
||||
lastCheck: string;
|
||||
formattedLastCheck: string;
|
||||
checksPassed: number;
|
||||
checksFailed: number;
|
||||
totalChecks: number;
|
||||
successRate: string;
|
||||
}
|
||||
|
||||
export interface HealthComponent {
|
||||
name: string;
|
||||
status: 'ok' | 'degraded' | 'error' | 'unknown';
|
||||
statusLabel: string;
|
||||
statusColor: string;
|
||||
statusIcon: string;
|
||||
lastCheck: string;
|
||||
formattedLastCheck: string;
|
||||
responseTime: string;
|
||||
errorRate: string;
|
||||
}
|
||||
|
||||
export interface HealthAlert {
|
||||
id: string;
|
||||
type: 'critical' | 'warning' | 'info';
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
formattedTimestamp: string;
|
||||
relativeTime: string;
|
||||
severity: string;
|
||||
severityColor: string;
|
||||
}
|
||||
|
||||
export interface HealthViewData {
|
||||
overallStatus: HealthStatus;
|
||||
metrics: HealthMetrics;
|
||||
components: HealthComponent[];
|
||||
alerts: HealthAlert[];
|
||||
hasAlerts: boolean;
|
||||
hasDegradedComponents: boolean;
|
||||
hasErrorComponents: boolean;
|
||||
lastUpdated: string;
|
||||
formattedLastUpdated: string;
|
||||
}
|
||||
Reference in New Issue
Block a user