UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

300 lines 11.4 kB
export { BaseService, CircuitBreaker, RetryHandler, CircuitBreakerState, defaultServiceConfig } from './base.js'; export { DatabaseService, databaseService } from './database.js'; export { SearchService, searchService } from './search.js'; export { ExternalApiService, externalApiService } from './external-api.js'; import { databaseService } from './database.js'; import { searchService } from './search.js'; import { externalApiService } from './external-api.js'; import { logger } from '../utils/logger.js'; export class ServiceManager { constructor(config = {}) { this.services = new Map(); this.healthMonitoringInterval = null; this.isShuttingDown = false; this.config = { enableHealthMonitoring: true, healthCheckInterval: 30000, // 30 seconds shutdownTimeout: 30000, // 30 seconds ...config }; } static getInstance(config) { if (!ServiceManager.instance) { ServiceManager.instance = new ServiceManager(config); } return ServiceManager.instance; } async initialize() { try { logger.info('Initializing service manager...'); // Register core services this.registerService('database', databaseService); this.registerService('search', searchService); this.registerService('externalApi', externalApiService); // Initialize all services const initPromises = Array.from(this.services.entries()).map(async ([name, service]) => { try { logger.info(`Initializing service: ${name}`); if ('initialize' in service && typeof service.initialize === 'function') { await service.initialize(); } logger.info(`Service initialized successfully: ${name}`); } catch (error) { logger.error(`Failed to initialize service ${name}:`, error); // Don't throw - allow partial initialization for graceful degradation } }); await Promise.allSettled(initPromises); // Start health monitoring if (this.config.enableHealthMonitoring) { this.startHealthMonitoring(); } logger.info('Service manager initialization completed'); } catch (error) { logger.error('Service manager initialization failed:', error); throw error; } } registerService(name, service) { if (this.services.has(name)) { logger.warn(`Service ${name} is already registered, replacing...`); } this.services.set(name, service); logger.debug(`Service registered: ${name}`); } getService(name) { return this.services.get(name); } getDatabaseService() { const service = this.getService('database'); if (!service) { throw new Error('Database service is not available'); } return service; } getSearchService() { const service = this.getService('search'); if (!service) { throw new Error('Search service is not available'); } return service; } getExternalApiService() { const service = this.getService('externalApi'); if (!service) { throw new Error('External API service is not available'); } return service; } async getSystemHealth() { const timestamp = new Date(); const services = {}; let healthyCount = 0; let totalRequests = 0; let totalErrors = 0; for (const [name, service] of this.services) { try { const healthStatus = service.getHealthStatus(); const metrics = service.getServiceMetrics(); const isHealthy = healthStatus.isHealthy && metrics.circuitBreakerState !== 'OPEN'; if (isHealthy) { healthyCount++; } services[name] = { isHealthy, status: isHealthy ? 'healthy' : 'unhealthy', responseTime: healthStatus.responseTime, error: healthStatus.error, lastCheck: healthStatus.lastCheck, metrics: { circuitBreakerState: metrics.circuitBreakerState, failureCount: metrics.failureCount } }; // Aggregate stats if available const serviceStats = service.getServiceStats?.(); if (serviceStats) { totalRequests += serviceStats.requestCount || serviceStats.queryCount || 0; totalErrors += serviceStats.errorCount || 0; services[name].metrics = { ...services[name].metrics, ...serviceStats }; } } catch (error) { logger.error(`Failed to get health status for service ${name}:`, error); services[name] = { isHealthy: false, status: 'error', error: error.message, lastCheck: timestamp }; } } const totalServices = this.services.size; const unhealthyServices = totalServices - healthyCount; const overallSuccessRate = totalRequests > 0 ? ((totalRequests - totalErrors) / totalRequests) * 100 : 100; return { isHealthy: unhealthyServices === 0, timestamp, services, summary: { totalServices, healthyServices: healthyCount, unhealthyServices, overallSuccessRate: Math.round(overallSuccessRate * 100) / 100 } }; } startHealthMonitoring() { if (this.healthMonitoringInterval) { return; // Already started } this.healthMonitoringInterval = setInterval(async () => { if (this.isShuttingDown) { return; } try { const healthReport = await this.getSystemHealth(); if (!healthReport.isHealthy) { const unhealthyServices = Object.entries(healthReport.services) .filter(([_, service]) => !service.isHealthy) .map(([name, _]) => name); logger.warn('System health check detected issues', { unhealthyServices, totalServices: healthReport.summary.totalServices, healthyServices: healthReport.summary.healthyServices }); } else { logger.debug('System health check passed', { totalServices: healthReport.summary.totalServices, successRate: healthReport.summary.overallSuccessRate }); } } catch (error) { logger.error('Health monitoring error:', error); } }, this.config.healthCheckInterval); logger.info(`Health monitoring started with ${this.config.healthCheckInterval}ms interval`); } stopHealthMonitoring() { if (this.healthMonitoringInterval) { clearInterval(this.healthMonitoringInterval); this.healthMonitoringInterval = null; logger.info('Health monitoring stopped'); } } async resetAllCircuitBreakers() { logger.info('Resetting all circuit breakers...'); for (const [name, service] of this.services) { try { service.resetCircuitBreaker(); logger.debug(`Circuit breaker reset for service: ${name}`); } catch (error) { logger.error(`Failed to reset circuit breaker for service ${name}:`, error); } } logger.info('All circuit breakers reset completed'); } async gracefulShutdown() { if (this.isShuttingDown) { return; } this.isShuttingDown = true; logger.info('Starting graceful shutdown of service manager...'); // Stop health monitoring this.stopHealthMonitoring(); // Shutdown all services with timeout const shutdownPromises = Array.from(this.services.entries()).map(async ([name, service]) => { try { logger.info(`Shutting down service: ${name}`); await service.shutdown(); logger.info(`Service shutdown completed: ${name}`); } catch (error) { logger.error(`Error shutting down service ${name}:`, error); } }); // Wait for all shutdowns with timeout const shutdownTimeout = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Shutdown timeout after ${this.config.shutdownTimeout}ms`)); }, this.config.shutdownTimeout); }); try { await Promise.race([ Promise.allSettled(shutdownPromises), shutdownTimeout ]); } catch (error) { logger.warn('Some services did not shutdown gracefully:', error.message); } logger.info('Service manager shutdown completed'); } getServiceList() { return Array.from(this.services.keys()); } isServiceRegistered(name) { return this.services.has(name); } getServiceCount() { return this.services.size; } } // Create and export singleton instance export const serviceManager = ServiceManager.getInstance(); // Graceful shutdown handler process.on('SIGTERM', async () => { logger.info('Received SIGTERM, starting graceful shutdown...'); try { await serviceManager.gracefulShutdown(); process.exit(0); } catch (error) { logger.error('Error during graceful shutdown:', error); process.exit(1); } }); process.on('SIGINT', async () => { logger.info('Received SIGINT, starting graceful shutdown...'); try { await serviceManager.gracefulShutdown(); process.exit(0); } catch (error) { logger.error('Error during graceful shutdown:', error); process.exit(1); } }); // Handle uncaught exceptions process.on('uncaughtException', async (error) => { logger.error('Uncaught exception:', error); try { await serviceManager.gracefulShutdown(); } catch (shutdownError) { logger.error('Error during emergency shutdown:', shutdownError); } process.exit(1); }); // Handle unhandled promise rejections process.on('unhandledRejection', async (reason, promise) => { logger.error('Unhandled promise rejection:', { reason, promise }); try { await serviceManager.gracefulShutdown(); } catch (shutdownError) { logger.error('Error during emergency shutdown:', shutdownError); } process.exit(1); }); //# sourceMappingURL=index.js.map