view data fixes
This commit is contained in:
349
apps/website/lib/gateways/api/base/ApiConnectionMonitor.ts
Normal file
349
apps/website/lib/gateways/api/base/ApiConnectionMonitor.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* API Connection Status Monitor and Health Checks
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export type ConnectionStatus = 'connected' | 'disconnected' | 'degraded' | 'checking';
|
||||
|
||||
export interface ConnectionHealth {
|
||||
status: ConnectionStatus;
|
||||
lastCheck: Date | null;
|
||||
lastSuccess: Date | null;
|
||||
lastFailure: Date | null;
|
||||
consecutiveFailures: number;
|
||||
totalRequests: number;
|
||||
successfulRequests: number;
|
||||
failedRequests: number;
|
||||
averageResponseTime: number;
|
||||
}
|
||||
|
||||
export interface HealthCheckResult {
|
||||
healthy: boolean;
|
||||
responseTime: number;
|
||||
error?: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export class ApiConnectionMonitor extends EventEmitter {
|
||||
private static instance: ApiConnectionMonitor;
|
||||
private health: ConnectionHealth;
|
||||
private isChecking = false;
|
||||
private checkInterval: NodeJS.Timeout | null = null;
|
||||
private healthCheckEndpoint: string;
|
||||
private readonly CHECK_INTERVAL = 300000; // 5 minutes
|
||||
private readonly DEGRADATION_THRESHOLD = 0.7; // 70% failure rate
|
||||
|
||||
private constructor(healthCheckEndpoint: string = '/health') {
|
||||
super();
|
||||
this.healthCheckEndpoint = healthCheckEndpoint;
|
||||
this.health = {
|
||||
status: 'disconnected',
|
||||
lastCheck: null,
|
||||
lastSuccess: null,
|
||||
lastFailure: null,
|
||||
consecutiveFailures: 0,
|
||||
totalRequests: 0,
|
||||
successfulRequests: 0,
|
||||
failedRequests: 0,
|
||||
averageResponseTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
static getInstance(healthCheckEndpoint?: string): ApiConnectionMonitor {
|
||||
if (!ApiConnectionMonitor.instance) {
|
||||
ApiConnectionMonitor.instance = new ApiConnectionMonitor(healthCheckEndpoint);
|
||||
}
|
||||
return ApiConnectionMonitor.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start automatic health monitoring
|
||||
*/
|
||||
startMonitoring(intervalMs?: number): void {
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval);
|
||||
}
|
||||
|
||||
const interval = intervalMs || this.CHECK_INTERVAL;
|
||||
this.checkInterval = setInterval(() => {
|
||||
this.performHealthCheck();
|
||||
}, interval);
|
||||
|
||||
// Initial check
|
||||
this.performHealthCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop automatic health monitoring
|
||||
*/
|
||||
stopMonitoring(): void {
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval);
|
||||
this.checkInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a manual health check
|
||||
*/
|
||||
async performHealthCheck(): Promise<HealthCheckResult> {
|
||||
if (this.isChecking) {
|
||||
return {
|
||||
healthy: false,
|
||||
responseTime: 0,
|
||||
error: 'Check already in progress',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
this.isChecking = true;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try multiple endpoints to determine actual connectivity
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const endpointsToTry = [
|
||||
`${baseUrl}${this.healthCheckEndpoint}`,
|
||||
`${baseUrl}/api/health`,
|
||||
`${baseUrl}/status`,
|
||||
baseUrl, // Root endpoint
|
||||
];
|
||||
|
||||
let lastError: Error | null = null;
|
||||
let successfulResponse: Response | null = null;
|
||||
|
||||
for (const endpoint of endpointsToTry) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
cache: 'no-store',
|
||||
// Add credentials to handle auth
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Consider any response (even 404) as connectivity success
|
||||
if (response.ok || response.status === 404 || response.status === 401) {
|
||||
successfulResponse = response;
|
||||
break;
|
||||
}
|
||||
} catch (endpointError) {
|
||||
lastError = endpointError as Error;
|
||||
// Try next endpoint
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
if (successfulResponse) {
|
||||
this.recordSuccess(responseTime);
|
||||
this.isChecking = false;
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
responseTime,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
} else {
|
||||
// If we got here, all endpoints failed
|
||||
const errorMessage = lastError?.message || 'All endpoints failed to respond';
|
||||
this.recordFailure(errorMessage);
|
||||
this.isChecking = false;
|
||||
|
||||
return {
|
||||
healthy: false,
|
||||
responseTime,
|
||||
error: errorMessage,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
this.recordFailure(errorMessage);
|
||||
this.isChecking = false;
|
||||
|
||||
return {
|
||||
healthy: false,
|
||||
responseTime,
|
||||
error: errorMessage,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a successful API request
|
||||
*/
|
||||
recordSuccess(responseTime: number = 0): void {
|
||||
this.health.totalRequests++;
|
||||
this.health.successfulRequests++;
|
||||
this.health.consecutiveFailures = 0;
|
||||
this.health.lastSuccess = new Date();
|
||||
this.health.lastCheck = new Date();
|
||||
|
||||
// Update average response time
|
||||
const total = this.health.successfulRequests;
|
||||
this.health.averageResponseTime =
|
||||
((this.health.averageResponseTime * (total - 1)) + responseTime) / total;
|
||||
|
||||
this.updateStatus();
|
||||
this.emit('success', { responseTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed API request
|
||||
*/
|
||||
recordFailure(error: string | Error): void {
|
||||
this.health.totalRequests++;
|
||||
this.health.failedRequests++;
|
||||
this.health.consecutiveFailures++;
|
||||
this.health.lastFailure = new Date();
|
||||
this.health.lastCheck = new Date();
|
||||
|
||||
this.updateStatus();
|
||||
this.emit('failure', {
|
||||
error: typeof error === 'string' ? error : error.message,
|
||||
consecutiveFailures: this.health.consecutiveFailures
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current connection health
|
||||
*/
|
||||
getHealth(): ConnectionHealth {
|
||||
return { ...this.health };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current connection status
|
||||
*/
|
||||
getStatus(): ConnectionStatus {
|
||||
return this.health.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API is currently available
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return this.health.status === 'connected' || this.health.status === 'degraded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reliability percentage
|
||||
*/
|
||||
getReliability(): number {
|
||||
if (this.health.totalRequests === 0) return 0;
|
||||
return (this.health.successfulRequests / this.health.totalRequests) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all statistics
|
||||
*/
|
||||
reset(): void {
|
||||
this.health = {
|
||||
status: 'disconnected',
|
||||
lastCheck: null,
|
||||
lastSuccess: null,
|
||||
lastFailure: null,
|
||||
consecutiveFailures: 0,
|
||||
totalRequests: 0,
|
||||
successfulRequests: 0,
|
||||
failedRequests: 0,
|
||||
averageResponseTime: 0,
|
||||
};
|
||||
this.emit('reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed status report for development
|
||||
*/
|
||||
getDebugReport(): string {
|
||||
const reliability = this.getReliability().toFixed(2);
|
||||
const avgTime = this.health.averageResponseTime.toFixed(2);
|
||||
|
||||
return `API Connection Status:
|
||||
Status: ${this.health.status}
|
||||
Reliability: ${reliability}%
|
||||
Total Requests: ${this.health.totalRequests}
|
||||
Successful: ${this.health.successfulRequests}
|
||||
Failed: ${this.health.failedRequests}
|
||||
Consecutive Failures: ${this.health.consecutiveFailures}
|
||||
Avg Response Time: ${avgTime}ms
|
||||
Last Check: ${this.health.lastCheck?.toISOString() || 'never'}
|
||||
Last Success: ${this.health.lastSuccess?.toISOString() || 'never'}
|
||||
Last Failure: ${this.health.lastFailure?.toISOString() || 'never'}`;
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
const reliability = this.health.totalRequests > 0
|
||||
? this.health.successfulRequests / this.health.totalRequests
|
||||
: 0;
|
||||
|
||||
// More nuanced status determination
|
||||
if (this.health.totalRequests === 0) {
|
||||
// No requests yet - don't assume disconnected
|
||||
this.health.status = 'checking';
|
||||
} else if (this.health.consecutiveFailures >= 3) {
|
||||
// Multiple consecutive failures indicates real connectivity issue
|
||||
this.health.status = 'disconnected';
|
||||
} else if (reliability < this.DEGRADATION_THRESHOLD && this.health.totalRequests >= 5) {
|
||||
// Only degrade if we have enough samples and reliability is low
|
||||
this.health.status = 'degraded';
|
||||
} else if (reliability >= this.DEGRADATION_THRESHOLD || this.health.successfulRequests > 0) {
|
||||
// If we have any successes, we're connected
|
||||
this.health.status = 'connected';
|
||||
} else {
|
||||
// Default to checking if uncertain
|
||||
this.health.status = 'checking';
|
||||
}
|
||||
|
||||
// Emit status change events (only on actual changes)
|
||||
if (this.health.status === 'disconnected') {
|
||||
this.emit('disconnected');
|
||||
} else if (this.health.status === 'degraded') {
|
||||
this.emit('degraded');
|
||||
} else if (this.health.status === 'connected') {
|
||||
this.emit('connected');
|
||||
} else if (this.health.status === 'checking') {
|
||||
this.emit('checking');
|
||||
}
|
||||
}
|
||||
|
||||
private getBaseUrl(): string {
|
||||
// Try to get base URL from environment or fallback
|
||||
if (typeof window !== 'undefined') {
|
||||
return process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
}
|
||||
return process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global connection status utility
|
||||
*/
|
||||
export const connectionMonitor = ApiConnectionMonitor.getInstance();
|
||||
|
||||
/**
|
||||
* Hook for React components to monitor connection status
|
||||
*/
|
||||
export function useConnectionStatus() {
|
||||
const monitor = ApiConnectionMonitor.getInstance();
|
||||
|
||||
return {
|
||||
status: monitor.getStatus(),
|
||||
health: monitor.getHealth(),
|
||||
isAvailable: monitor.isAvailable(),
|
||||
reliability: monitor.getReliability(),
|
||||
checkHealth: () => monitor.performHealthCheck(),
|
||||
getDebugReport: () => monitor.getDebugReport(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user