import { getDb } from '../../utils/db' export default defineEventHandler(async (event) => { const db = getDb() const SLOPE_THRESHOLD = 0.5 // 분당 0.5% 이상 증가/감소 시 이상 const MIN_SAMPLES = 10 // 최소 10개 샘플 필요 const WINDOW_MINUTES = 30 // 30분 윈도우 const servers = db.prepare(` SELECT target_id, server_name FROM server_targets WHERE is_active = 1 ORDER BY server_name `).all() as any[] const anomalies: any[] = [] const serverResults: any[] = [] // 로그 저장용 const insertLog = db.prepare(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) VALUES (?, ?, 'trend', ?, ?, ?, ?, ?) `) const recentLogExists = db.prepare(` SELECT 1 FROM anomaly_logs WHERE target_id = ? AND detect_type = 'trend' AND metric = ? AND detected_at > datetime('now', '-1 minute', 'localtime') LIMIT 1 `) for (const server of servers) { // 최근 30분 데이터 조회 const snapshots = db.prepare(` SELECT cpu_percent, memory_percent, collected_at, (julianday('now', 'localtime') - julianday(collected_at)) * 24 * 60 as minutes_ago FROM server_snapshots WHERE target_id = ? AND is_online = 1 AND collected_at >= datetime('now', '-${WINDOW_MINUTES} minutes', 'localtime') ORDER BY collected_at ASC `).all(server.target_id) as any[] if (snapshots.length < MIN_SAMPLES) { serverResults.push({ target_id: server.target_id, server_name: server.server_name, cpu_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].cpu_percent : null, mem_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].memory_percent : null, cpu_slope: null, mem_slope: null, cpu_trend: null, mem_trend: null, sample_count: snapshots.length, status: 'insufficient' }) continue } // 선형 회귀 계산 (최소제곱법) // y = ax + b, a = slope (기울기) const n = snapshots.length const current = snapshots[n - 1] const currCpu = current.cpu_percent ?? 0 const currMem = current.memory_percent ?? 0 // x = 시간 (분), y = 값 const cpuPoints = snapshots.map((s, i) => ({ x: i, y: s.cpu_percent ?? 0 })) const memPoints = snapshots.map((s, i) => ({ x: i, y: s.memory_percent ?? 0 })) function linearRegression(points: { x: number, y: number }[]): { slope: number, intercept: number, r2: number } { const n = points.length let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0 for (const p of points) { sumX += p.x sumY += p.y sumXY += p.x * p.y sumX2 += p.x * p.x sumY2 += p.y * p.y } const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX) const intercept = (sumY - slope * sumX) / n // R² (결정계수) 계산 const yMean = sumY / n let ssTotal = 0, ssResidual = 0 for (const p of points) { const yPred = slope * p.x + intercept ssTotal += Math.pow(p.y - yMean, 2) ssResidual += Math.pow(p.y - yPred, 2) } const r2 = ssTotal > 0 ? 1 - (ssResidual / ssTotal) : 0 return { slope, intercept, r2 } } const cpuReg = linearRegression(cpuPoints) const memReg = linearRegression(memPoints) // 분당 기울기로 환산 (수집 간격 고려) const totalMinutes = WINDOW_MINUTES const cpuSlopePerMin = (cpuReg.slope * n) / totalMinutes const memSlopePerMin = (memReg.slope * n) / totalMinutes // 추세 판단 function getTrend(slope: number, r2: number): string { if (r2 < 0.3) return 'unstable' // 추세가 불안정 if (slope >= SLOPE_THRESHOLD) return 'rising' if (slope <= -SLOPE_THRESHOLD) return 'falling' return 'stable' } const cpuTrend = getTrend(cpuSlopePerMin, cpuReg.r2) const memTrend = getTrend(memSlopePerMin, memReg.r2) // 상태 결정 let status = 'normal' if (cpuTrend === 'rising' || memTrend === 'rising') status = 'warning' if (cpuSlopePerMin >= 1.0 || memSlopePerMin >= 1.0) status = 'danger' // 분당 1% 이상 serverResults.push({ target_id: server.target_id, server_name: server.server_name, cpu_current: currCpu, mem_current: currMem, cpu_slope: cpuSlopePerMin, mem_slope: memSlopePerMin, cpu_trend: cpuTrend, mem_trend: memTrend, cpu_r2: cpuReg.r2, mem_r2: memReg.r2, sample_count: snapshots.length, status }) // CPU 이상감지 + 로그 저장 if (cpuTrend === 'rising' && cpuReg.r2 >= 0.3) { const level = cpuSlopePerMin >= 1.0 ? 'danger' : 'warning' const message = `CPU 지속 상승 중 (분당 +${cpuSlopePerMin.toFixed(2)}%, R²=${cpuReg.r2.toFixed(2)})` anomalies.push({ target_id: server.target_id, server_name: server.server_name, metric: 'CPU', current: currCpu, slope: cpuSlopePerMin, r2: cpuReg.r2, trend: cpuTrend, level }) if (!recentLogExists.get(server.target_id, 'CPU')) { insertLog.run(server.target_id, server.server_name, 'CPU', level, currCpu, cpuSlopePerMin, message) } } // Memory 이상감지 + 로그 저장 if (memTrend === 'rising' && memReg.r2 >= 0.3) { const level = memSlopePerMin >= 1.0 ? 'danger' : 'warning' const message = `Memory 지속 상승 중 (분당 +${memSlopePerMin.toFixed(2)}%, R²=${memReg.r2.toFixed(2)})` anomalies.push({ target_id: server.target_id, server_name: server.server_name, metric: 'Memory', current: currMem, slope: memSlopePerMin, r2: memReg.r2, trend: memTrend, level }) if (!recentLogExists.get(server.target_id, 'Memory')) { insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memSlopePerMin, message) } } } anomalies.sort((a, b) => b.slope - a.slope) return { anomalies, servers: serverResults, config: { slope_threshold: SLOPE_THRESHOLD, window_minutes: WINDOW_MINUTES, min_samples: MIN_SAMPLES }, timestamp: new Date().toISOString() } })