UNPKG

@rhofkens/mcp-quotes-server-claude-code

Version:

Model Context Protocol (MCP) server for managing and serving quotes

288 lines 9.61 kB
/** * Health Check and Status Monitoring * * Provides health checking capabilities for external services * and internal system components */ import axios from 'axios'; import { CircuitState } from './circuitBreaker.js'; import { logger } from './logger.js'; /** * Health status levels */ export var HealthStatus; (function (HealthStatus) { HealthStatus["HEALTHY"] = "healthy"; HealthStatus["DEGRADED"] = "degraded"; HealthStatus["UNHEALTHY"] = "unhealthy"; })(HealthStatus || (HealthStatus = {})); /** * Health check manager */ export class HealthCheckManager { checks; lastResults; checkInterval; startTime; config; constructor(config = {}) { this.checks = new Map(); this.lastResults = new Map(); this.startTime = Date.now(); this.config = { interval: config.interval || 60000, // 1 minute default timeout: config.timeout || 10000, // 10 seconds default includeDetails: config.includeDetails !== false, }; } /** * Register a health check */ register(name, check) { this.checks.set(name, check); logger.info('Health check registered', { name }); } /** * Unregister a health check */ unregister(name) { const result = this.checks.delete(name); this.lastResults.delete(name); return result; } /** * Run all health checks */ async runChecks() { const results = []; // Run all checks in parallel with timeout const checkPromises = Array.from(this.checks.entries()).map(async ([name, check]) => { const startTime = Date.now(); try { const result = await Promise.race([check(), this.timeout(name)]); result.responseTime = Date.now() - startTime; this.lastResults.set(name, result); return result; } catch (error) { const errorResult = { name, status: HealthStatus.UNHEALTHY, message: error instanceof Error ? error.message : 'Health check failed', lastChecked: new Date(), responseTime: Date.now() - startTime, }; this.lastResults.set(name, errorResult); return errorResult; } }); results.push(...(await Promise.all(checkPromises))); // Determine overall status const hasUnhealthy = results.some((r) => r.status === HealthStatus.UNHEALTHY); const hasDegraded = results.some((r) => r.status === HealthStatus.DEGRADED); let overallStatus; if (hasUnhealthy) { overallStatus = HealthStatus.UNHEALTHY; } else if (hasDegraded) { overallStatus = HealthStatus.DEGRADED; } else { overallStatus = HealthStatus.HEALTHY; } return { status: overallStatus, timestamp: new Date(), components: results, uptime: Date.now() - this.startTime, version: process.env['npm_package_version'] || 'unknown', }; } /** * Get last health check results */ getLastResults() { const components = Array.from(this.lastResults.values()); const hasUnhealthy = components.some((c) => c.status === HealthStatus.UNHEALTHY); const hasDegraded = components.some((c) => c.status === HealthStatus.DEGRADED); return { status: hasUnhealthy ? HealthStatus.UNHEALTHY : hasDegraded ? HealthStatus.DEGRADED : HealthStatus.HEALTHY, timestamp: new Date(), components, uptime: Date.now() - this.startTime, version: process.env['npm_package_version'] || 'unknown', }; } /** * Start periodic health checks */ startPeriodicChecks() { if (this.checkInterval) { return; // Already running } // Run initial check this.runChecks().catch((error) => { logger.error('Initial health check failed', error); }); // Schedule periodic checks this.checkInterval = setInterval(() => { this.runChecks().catch((error) => { logger.error('Periodic health check failed', error); }); }, this.config.interval); logger.info('Periodic health checks started', { interval: this.config.interval, }); } /** * Stop periodic health checks */ stopPeriodicChecks() { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = undefined; logger.info('Periodic health checks stopped'); } } /** * Timeout helper for health checks */ timeout(name) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Health check '${name}' timed out after ${this.config.timeout}ms`)); }, this.config.timeout); }); } } /** * Built-in health checks */ /** * Create Serper API health check */ export function createSerperHealthCheck(apiKey, baseUrl = 'https://google.serper.dev') { return async () => { const startTime = Date.now(); try { // Simple search to test API const response = await axios.post(`${baseUrl}/search`, { q: 'test', num: 1 }, { headers: { 'X-API-KEY': apiKey }, timeout: 5000, }); const responseTime = Date.now() - startTime; if (response.status === 200 && !('error' in response.data)) { return { name: 'serper-api', status: HealthStatus.HEALTHY, message: 'Serper API is responding normally', details: { responseTime }, lastChecked: new Date(), responseTime, }; } else { return { name: 'serper-api', status: HealthStatus.DEGRADED, message: 'Serper API returned unexpected response', details: { status: response.status, error: 'error' in response.data ? response.data.error : undefined, }, lastChecked: new Date(), responseTime, }; } } catch (error) { return { name: 'serper-api', status: HealthStatus.UNHEALTHY, message: error instanceof Error ? error.message : 'Serper API health check failed', lastChecked: new Date(), responseTime: Date.now() - startTime, }; } }; } /** * Create cache health check */ export function createCacheHealthCheck(getStats) { return () => { const stats = getStats(); const hitRate = stats.hits + stats.misses > 0 ? stats.hits / (stats.hits + stats.misses) : 0; let status; let message; if (hitRate < 0.1 && stats.hits + stats.misses > 100) { status = HealthStatus.DEGRADED; message = 'Cache hit rate is very low'; } else if (stats.size > 900 && stats.evictions > 100) { status = HealthStatus.DEGRADED; message = 'Cache is near capacity with high eviction rate'; } else { status = HealthStatus.HEALTHY; message = 'Cache is operating normally'; } return Promise.resolve({ name: 'cache', status, message, details: { ...stats, hitRate: Math.round(hitRate * 100) / 100, }, lastChecked: new Date(), }); }; } /** * Create circuit breaker health check */ export function createCircuitBreakerHealthCheck(name, getStats) { return () => { const stats = getStats(); let status; let message; switch (stats.state) { case CircuitState.OPEN: status = HealthStatus.UNHEALTHY; message = 'Circuit breaker is open'; break; case CircuitState.HALF_OPEN: status = HealthStatus.DEGRADED; message = 'Circuit breaker is half-open (testing recovery)'; break; case CircuitState.CLOSED: if (stats.failures > 0) { status = HealthStatus.DEGRADED; message = `Circuit breaker has ${stats.failures} recent failures`; } else { status = HealthStatus.HEALTHY; message = 'Circuit breaker is functioning normally'; } break; default: status = HealthStatus.HEALTHY; message = 'Circuit breaker is functioning normally'; } return Promise.resolve({ name: `circuit-breaker-${name}`, status, message, details: { ...stats }, lastChecked: new Date(), }); }; } // Export singleton health check manager export const healthCheckManager = new HealthCheckManager(); //# sourceMappingURL=healthCheck.js.map