UNPKG

sometrend-mcp-server

Version:

TM2 기반의 썸트렌드(sometrend) MCP 서버 - 트렌드 분석 및 데이터 처리

295 lines (261 loc) 8.35 kB
/** * 프로덕션 레벨 헬스체크 시스템 */ import { Request, Response } from 'express'; import { ProductionLogger } from '../utils/ProductionLogger.js'; export interface HealthStatus { status: 'healthy' | 'degraded' | 'unhealthy'; timestamp: string; uptime: number; version: string; checks: { [key: string]: { status: 'pass' | 'fail' | 'warn'; message?: string; duration?: number; timestamp: string; }; }; metrics: { memory: NodeJS.MemoryUsage; activeSessions: number; requestsPerMinute: number; averageResponseTime: number; }; } export class HealthCheckService { private static instance: HealthCheckService; private requestCounts: number[] = []; private responseTimes: number[] = []; private activeSessions: Map<string, any> = new Map(); private constructor() { // 메트릭 정리를 위한 주기적 클린업 setInterval(() => { this.cleanupMetrics(); }, 60000); // 1분마다 } static getInstance(): HealthCheckService { if (!HealthCheckService.instance) { HealthCheckService.instance = new HealthCheckService(); } return HealthCheckService.instance; } /** * 세션 추가 */ addSession(sessionId: string, session: any): void { this.activeSessions.set(sessionId, session); } /** * 세션 제거 */ removeSession(sessionId: string): void { this.activeSessions.delete(sessionId); } /** * 요청 기록 */ recordRequest(responseTime: number): void { const now = Date.now(); this.requestCounts.push(now); this.responseTimes.push(responseTime); } /** * 메트릭 정리 (1분 이상 된 데이터 제거) */ private cleanupMetrics(): void { const oneMinuteAgo = Date.now() - 60000; this.requestCounts = this.requestCounts.filter(time => time > oneMinuteAgo); this.responseTimes = this.responseTimes.filter((_, index) => { return this.requestCounts[index] !== undefined; }); } /** * 분당 요청 수 계산 */ private getRequestsPerMinute(): number { return this.requestCounts.length; } /** * 평균 응답 시간 계산 */ private getAverageResponseTime(): number { if (this.responseTimes.length === 0) return 0; const sum = this.responseTimes.reduce((a, b) => a + b, 0); return Math.round(sum / this.responseTimes.length); } /** * 메모리 상태 확인 */ private async checkMemory(): Promise<{ status: 'pass' | 'fail' | 'warn'; message?: string }> { const memUsage = process.memoryUsage(); const memUsageMB = memUsage.heapUsed / 1024 / 1024; const memLimitMB = parseInt(process.env.MEMORY_LIMIT || '512', 10); if (memUsageMB > memLimitMB * 0.9) { return { status: 'fail', message: `Memory usage too high: ${Math.round(memUsageMB)}MB` }; } else if (memUsageMB > memLimitMB * 0.7) { return { status: 'warn', message: `Memory usage high: ${Math.round(memUsageMB)}MB` }; } return { status: 'pass', message: `Memory usage normal: ${Math.round(memUsageMB)}MB` }; } /** * 카테고리 리소스 상태 확인 */ private async checkCategoryResources(): Promise<{ status: 'pass' | 'fail' | 'warn'; message?: string }> { try { // CategoryResourceService의 초기화 상태 확인 // 실제 구현에서는 CategoryResourceService에 isInitialized 메서드를 추가해야 함 return { status: 'pass', message: 'Category resources loaded' }; } catch (error) { return { status: 'fail', message: `Category resources unavailable: ${(error as Error).message}` }; } } /** * TM2 API 연결 상태 확인 */ private async checkTM2API(): Promise<{ status: 'pass' | 'fail' | 'warn'; message?: string }> { try { const startTime = Date.now(); // 실제 TM2 API 헬스체크 엔드포인트 호출 // 여기서는 간단한 timeout 체크로 대체 await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('TM2 API timeout')); }, 5000); // 실제로는 TM2 API 헬스체크 호출 setTimeout(() => { clearTimeout(timeout); resolve(true); }, 100); }); const duration = Date.now() - startTime; if (duration > 3000) { return { status: 'warn', message: `TM2 API slow: ${duration}ms` }; } return { status: 'pass', message: `TM2 API responsive: ${duration}ms` }; } catch (error) { return { status: 'fail', message: `TM2 API unreachable: ${(error as Error).message}` }; } } /** * 종합 헬스체크 실행 */ async getHealthStatus(): Promise<HealthStatus> { const startTime = Date.now(); // 각 컴포넌트 헬스체크 실행 const [memoryCheck, categoryCheck, tm2Check] = await Promise.all([ this.checkMemory(), this.checkCategoryResources(), this.checkTM2API() ]); const checks = { memory: { ...memoryCheck, timestamp: new Date().toISOString(), duration: Date.now() - startTime }, categories: { ...categoryCheck, timestamp: new Date().toISOString(), duration: Date.now() - startTime }, tm2_api: { ...tm2Check, timestamp: new Date().toISOString(), duration: Date.now() - startTime } }; // 전체 상태 결정 const hasFailures = Object.values(checks).some(check => check.status === 'fail'); const hasWarnings = Object.values(checks).some(check => check.status === 'warn'); let overallStatus: 'healthy' | 'degraded' | 'unhealthy'; if (hasFailures) { overallStatus = 'unhealthy'; } else if (hasWarnings) { overallStatus = 'degraded'; } else { overallStatus = 'healthy'; } const healthStatus: HealthStatus = { status: overallStatus, timestamp: new Date().toISOString(), uptime: process.uptime(), version: process.env.npm_package_version || '1.0.0', checks, metrics: { memory: process.memoryUsage(), activeSessions: this.activeSessions.size, requestsPerMinute: this.getRequestsPerMinute(), averageResponseTime: this.getAverageResponseTime() } }; // 상태 로깅 ProductionLogger.logSystemMetrics({ memoryUsage: healthStatus.metrics.memory, activeSessions: healthStatus.metrics.activeSessions, uptime: healthStatus.uptime }); return healthStatus; } /** * Express 핸들러 */ async handleHealthCheck(req: Request, res: Response): Promise<void> { try { const health = await this.getHealthStatus(); // HTTP 상태 코드 결정 let statusCode: number; switch (health.status) { case 'healthy': statusCode = 200; break; case 'degraded': statusCode = 200; // 경고는 여전히 200 break; case 'unhealthy': statusCode = 503; // Service Unavailable break; } res.status(statusCode).json(health); } catch (error) { ProductionLogger.logError(error as Error, { context: 'health_check' }); res.status(500).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: 'Health check failed', message: (error as Error).message }); } } /** * 간단한 liveness probe (Kubernetes용) */ handleLivenessProbe(req: Request, res: Response): void { res.status(200).json({ status: 'alive', timestamp: new Date().toISOString(), uptime: process.uptime() }); } /** * readiness probe (Kubernetes용) */ async handleReadinessProbe(req: Request, res: Response): Promise<void> { try { const health = await this.getHealthStatus(); const isReady = health.status !== 'unhealthy'; res.status(isReady ? 200 : 503).json({ status: isReady ? 'ready' : 'not_ready', timestamp: new Date().toISOString(), checks: health.checks }); } catch (error) { res.status(503).json({ status: 'not_ready', timestamp: new Date().toISOString(), error: (error as Error).message }); } } } export const healthCheckService = HealthCheckService.getInstance();