sometrend-mcp-server
Version:
TM2 기반의 썸트렌드(sometrend) MCP 서버 - 트렌드 분석 및 데이터 처리
295 lines (261 loc) • 8.35 kB
text/typescript
/**
* 프로덕션 레벨 헬스체크 시스템
*/
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();