From 9d2a6638b599456039de1ed7b06ff1da1bfd00f1 Mon Sep 17 00:00:00 2001 From: Hyoseong Jo Date: Sun, 28 Dec 2025 14:06:32 +0900 Subject: [PATCH] =?UTF-8?q?Docker=20=ED=8C=8C=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/anomaly/baseline.get.ts | 94 ++++++++++----------------- backend/api/anomaly/chart.get.ts | 76 +++++++++++----------- backend/api/anomaly/logs.get.ts | 40 ++++++------ backend/api/anomaly/short-term.get.ts | 66 +++++-------------- backend/api/anomaly/trend.get.ts | 88 +++++++------------------ backend/api/anomaly/zscore.get.ts | 70 +++++++------------- 6 files changed, 158 insertions(+), 276 deletions(-) diff --git a/backend/api/anomaly/baseline.get.ts b/backend/api/anomaly/baseline.get.ts index 3f043ed..4bcb649 100644 --- a/backend/api/anomaly/baseline.get.ts +++ b/backend/api/anomaly/baseline.get.ts @@ -1,15 +1,14 @@ -import { getDb } from '../../utils/db' +import { query } from '../../utils/db' export default defineEventHandler(async (event) => { - const db = getDb() const DEVIATION_THRESHOLD = 2.0 - const servers = db.prepare(` - SELECT target_id, server_name + const servers = await query(` + SELECT target_id, name as server_name FROM server_targets WHERE is_active = 1 - ORDER BY server_name - `).all() as any[] + ORDER BY name + `) const now = new Date() const currentHour = now.getHours() @@ -20,41 +19,32 @@ export default defineEventHandler(async (event) => { 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 (?, ?, 'baseline', ?, ?, ?, ?, ?) - `) - - const recentLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'baseline' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - for (const server of servers) { - const historicalData = db.prepare(` - SELECT cpu_percent, memory_percent, collected_at - FROM server_snapshots - WHERE target_id = ? - AND collected_at >= datetime('now', '-14 days', 'localtime') - AND strftime('%H', collected_at) = ? + // 최근 14일 동일 시간대 데이터 + const historicalData = await query(` + SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at + FROM server_logs + WHERE target_id = $1 + AND checked_at >= NOW() - INTERVAL '14 days' + AND EXTRACT(HOUR FROM checked_at) = $2 AND ( - (? = 'weekend' AND strftime('%w', collected_at) IN ('0', '6')) + ($3 = 'weekend' AND EXTRACT(DOW FROM checked_at) IN (0, 6)) OR - (? = 'weekday' AND strftime('%w', collected_at) NOT IN ('0', '6')) + ($3 = 'weekday' AND EXTRACT(DOW FROM checked_at) NOT IN (0, 6)) ) - ORDER BY collected_at DESC - `).all(server.target_id, currentHour.toString().padStart(2, '0'), dayType, dayType) as any[] + ORDER BY checked_at DESC + `, [server.target_id, currentHour, dayType]) - const current = db.prepare(` - SELECT cpu_percent, memory_percent - FROM server_snapshots - WHERE target_id = ? - ORDER BY collected_at DESC + // 현재 값 + const currentRows = await query(` + SELECT cpu_usage as cpu_percent, memory_usage as memory_percent + FROM server_logs + WHERE target_id = $1 + ORDER BY checked_at DESC LIMIT 1 - `).get(server.target_id) as any + `, [server.target_id]) + + const current = currentRows[0] if (!current || historicalData.length < 5) { serverResults.push({ @@ -74,17 +64,17 @@ export default defineEventHandler(async (event) => { continue } - const currCpu = current.cpu_percent ?? 0 - const currMem = current.memory_percent ?? 0 + const currCpu = Number(current.cpu_percent) || 0 + const currMem = Number(current.memory_percent) || 0 - const cpuValues = historicalData.map(s => s.cpu_percent ?? 0) - const memValues = historicalData.map(s => s.memory_percent ?? 0) + const cpuValues = historicalData.map((s: any) => Number(s.cpu_percent) || 0) + const memValues = historicalData.map((s: any) => Number(s.memory_percent) || 0) - const cpuAvg = cpuValues.reduce((a, b) => a + b, 0) / cpuValues.length - const memAvg = memValues.reduce((a, b) => a + b, 0) / memValues.length + const cpuAvg = cpuValues.reduce((a: number, b: number) => a + b, 0) / cpuValues.length + const memAvg = memValues.reduce((a: number, b: number) => a + b, 0) / memValues.length - const cpuVariance = cpuValues.reduce((sum, val) => sum + Math.pow(val - cpuAvg, 2), 0) / cpuValues.length - const memVariance = memValues.reduce((sum, val) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length + const cpuVariance = cpuValues.reduce((sum: number, val: number) => sum + Math.pow(val - cpuAvg, 2), 0) / cpuValues.length + const memVariance = memValues.reduce((sum: number, val: number) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length const cpuStd = Math.sqrt(cpuVariance) const memStd = Math.sqrt(memVariance) @@ -111,12 +101,9 @@ export default defineEventHandler(async (event) => { status }) - // CPU 이상감지 + 로그 저장 + // CPU 이상감지 if (Math.abs(cpuDeviation) >= DEVIATION_THRESHOLD) { const level = Math.abs(cpuDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = cpuDeviation >= 0 ? '높음' : '낮음' - const dayLabel = isWeekend ? '주말' : '평일' - const message = `CPU ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(cpuDeviation).toFixed(1)}σ ${direction} (기준: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` anomalies.push({ target_id: server.target_id, @@ -130,18 +117,11 @@ export default defineEventHandler(async (event) => { hour: currentHour, day_type: dayType }) - - if (!recentLogExists.get(server.target_id, 'CPU')) { - insertLog.run(server.target_id, server.server_name, 'CPU', level, currCpu, cpuDeviation, message) - } } - // Memory 이상감지 + 로그 저장 + // Memory 이상감지 if (Math.abs(memDeviation) >= DEVIATION_THRESHOLD) { const level = Math.abs(memDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = memDeviation >= 0 ? '높음' : '낮음' - const dayLabel = isWeekend ? '주말' : '평일' - const message = `Memory ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(memDeviation).toFixed(1)}σ ${direction} (기준: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` anomalies.push({ target_id: server.target_id, @@ -155,10 +135,6 @@ export default defineEventHandler(async (event) => { hour: currentHour, day_type: dayType }) - - if (!recentLogExists.get(server.target_id, 'Memory')) { - insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memDeviation, message) - } } } diff --git a/backend/api/anomaly/chart.get.ts b/backend/api/anomaly/chart.get.ts index ff281da..e0362f8 100644 --- a/backend/api/anomaly/chart.get.ts +++ b/backend/api/anomaly/chart.get.ts @@ -1,57 +1,59 @@ -import { getDb } from '../../utils/db' +import { query } from '../../utils/db' export default defineEventHandler(async (event) => { - const db = getDb() - const query = getQuery(event) + const queryParams = getQuery(event) - const type = query.type as string || 'short-term' - const period = query.period as string || '24h' + const type = queryParams.type as string || 'short-term' + const period = queryParams.period as string || '24h' // 기간/간격 계산 - let intervalClause = '' - let groupFormat = '' + let interval = '24 hours' + let groupFormat = 'YYYY-MM-DD HH24:00' if (period === '1h') { - intervalClause = `'-1 hours'` - groupFormat = '%Y-%m-%d %H:%M' // 분 단위 + interval = '1 hour' + groupFormat = 'YYYY-MM-DD HH24:MI' } else if (period === '6h') { - intervalClause = `'-6 hours'` - groupFormat = '%Y-%m-%d %H:00' // 시간 단위 + interval = '6 hours' + groupFormat = 'YYYY-MM-DD HH24:00' } else if (period === '12h') { - intervalClause = `'-12 hours'` - groupFormat = '%Y-%m-%d %H:00' + interval = '12 hours' + groupFormat = 'YYYY-MM-DD HH24:00' } else if (period === '24h') { - intervalClause = `'-24 hours'` - groupFormat = '%Y-%m-%d %H:00' + interval = '24 hours' + groupFormat = 'YYYY-MM-DD HH24:00' } else if (period === '7d') { - intervalClause = `'-7 days'` - groupFormat = '%Y-%m-%d' // 일 단위 + interval = '7 days' + groupFormat = 'YYYY-MM-DD' } else if (period === '30d') { - intervalClause = `'-30 days'` - groupFormat = '%Y-%m-%d' - } else { - intervalClause = `'-24 hours'` - groupFormat = '%Y-%m-%d %H:00' + interval = '30 days' + groupFormat = 'YYYY-MM-DD' } - // 시간대별 집계 - const rows = db.prepare(` - SELECT - strftime('${groupFormat}', detected_at) as time_slot, - SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning, - SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger - FROM anomaly_logs - WHERE detect_type = ? - AND detected_at >= datetime('now', ${intervalClause}, 'localtime') - GROUP BY time_slot - ORDER BY time_slot ASC - `).all(type) as any[] + // 시간대별 집계 (anomaly_logs 테이블이 없으면 빈 배열 반환) + let rows: any[] = [] + try { + rows = await query(` + SELECT + to_char(detected_at, '${groupFormat}') as time_slot, + SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning, + SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger + FROM anomaly_logs + WHERE detect_type = $1 + AND detected_at >= NOW() - INTERVAL '${interval}' + GROUP BY time_slot + ORDER BY time_slot ASC + `, [type]) + } catch (e) { + // 테이블이 없으면 빈 배열 + rows = [] + } // 시간 포맷 변환 const data = rows.map(r => ({ time: formatTimeLabel(r.time_slot, period), - warning: r.warning, - danger: r.danger + warning: Number(r.warning), + danger: Number(r.danger) })) return { @@ -66,11 +68,9 @@ function formatTimeLabel(timeSlot: string, period: string): string { if (!timeSlot) return '' if (period === '7d' || period === '30d') { - // 일 단위: MM/DD const parts = timeSlot.split('-') return `${parts[1]}/${parts[2]}` } else { - // 시간 단위: HH:MM const parts = timeSlot.split(' ') if (parts.length === 2) { return parts[1].substring(0, 5) diff --git a/backend/api/anomaly/logs.get.ts b/backend/api/anomaly/logs.get.ts index 0772b1f..8152c8e 100644 --- a/backend/api/anomaly/logs.get.ts +++ b/backend/api/anomaly/logs.get.ts @@ -1,33 +1,35 @@ -import { getDb } from '../../utils/db' +import { query } from '../../utils/db' export default defineEventHandler(async (event) => { - const db = getDb() - const query = getQuery(event) + const queryParams = getQuery(event) - const type = query.type as string || 'short-term' - const period = query.period as string || '24h' + const type = queryParams.type as string || 'short-term' + const period = queryParams.period as string || '24h' // 기간 계산 - let intervalClause = '' + let interval = '24 hours' if (period.endsWith('h')) { const hours = parseInt(period) - intervalClause = `'-${hours} hours'` + interval = `${hours} hours` } else if (period.endsWith('d')) { const days = parseInt(period) - intervalClause = `'-${days} days'` - } else { - intervalClause = `'-24 hours'` + interval = `${days} days` } - const logs = db.prepare(` - SELECT id, target_id, server_name, detect_type, metric, level, - current_value, threshold_value, message, detected_at - FROM anomaly_logs - WHERE detect_type = ? - AND detected_at >= datetime('now', ${intervalClause}, 'localtime') - ORDER BY detected_at DESC - LIMIT 100 - `).all(type) as any[] + let logs: any[] = [] + try { + logs = await query(` + SELECT id, target_id, server_name, detect_type, metric, level, + current_value, threshold_value, message, detected_at + FROM anomaly_logs + WHERE detect_type = $1 + AND detected_at >= NOW() - INTERVAL '${interval}' + ORDER BY detected_at DESC + LIMIT 100 + `, [type]) + } catch (e) { + logs = [] + } return { logs, diff --git a/backend/api/anomaly/short-term.get.ts b/backend/api/anomaly/short-term.get.ts index dea2064..180658c 100644 --- a/backend/api/anomaly/short-term.get.ts +++ b/backend/api/anomaly/short-term.get.ts @@ -1,42 +1,27 @@ -import { getDb } from '../../utils/db' +import { query } from '../../utils/db' export default defineEventHandler(async (event) => { - const db = getDb() const THRESHOLD = 30 // 30% 이상 변화 시 이상 감지 // 활성 서버 목록 - const servers = db.prepare(` - SELECT target_id, server_name + const servers = await query(` + SELECT target_id, name as server_name FROM server_targets WHERE is_active = 1 - ORDER BY server_name - `).all() as any[] + ORDER BY name + `) const anomalies: any[] = [] const serverResults: any[] = [] - // 로그 저장용 prepared statement - const insertLog = db.prepare(` - INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) - VALUES (?, ?, 'short-term', ?, ?, ?, ?, ?) - `) - - // 최근 1분 내 동일 로그 존재 여부 확인 (중복 방지) - const recentLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'short-term' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - for (const server of servers) { - const snapshots = db.prepare(` - SELECT cpu_percent, memory_percent, collected_at - FROM server_snapshots - WHERE target_id = ? - ORDER BY collected_at DESC + const snapshots = await query(` + SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at + FROM server_logs + WHERE target_id = $1 + ORDER BY checked_at DESC LIMIT 20 - `).all(server.target_id) as any[] + `, [server.target_id]) if (snapshots.length < 4) { serverResults.push({ @@ -53,10 +38,10 @@ export default defineEventHandler(async (event) => { const currSnapshots = snapshots.slice(0, half) const prevSnapshots = snapshots.slice(half) - const currCpuAvg = currSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / currSnapshots.length - const prevCpuAvg = prevSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / prevSnapshots.length - const currMemAvg = currSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / currSnapshots.length - const prevMemAvg = prevSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / prevSnapshots.length + const currCpuAvg = currSnapshots.reduce((sum: number, s: any) => sum + (Number(s.cpu_percent) || 0), 0) / currSnapshots.length + const prevCpuAvg = prevSnapshots.reduce((sum: number, s: any) => sum + (Number(s.cpu_percent) || 0), 0) / prevSnapshots.length + const currMemAvg = currSnapshots.reduce((sum: number, s: any) => sum + (Number(s.memory_percent) || 0), 0) / currSnapshots.length + const prevMemAvg = prevSnapshots.reduce((sum: number, s: any) => sum + (Number(s.memory_percent) || 0), 0) / prevSnapshots.length let cpuChange: number | null = null let memChange: number | null = null @@ -86,12 +71,8 @@ export default defineEventHandler(async (event) => { status }) - // CPU 이상 감지 + 로그 저장 + // CPU 이상 감지 if (cpuChange !== null && Math.abs(cpuChange) >= THRESHOLD) { - const level = Math.abs(cpuChange) >= 100 ? 'danger' : 'warning' - const direction = cpuChange >= 0 ? '증가' : '감소' - const message = `CPU ${direction} 감지 (${prevCpuAvg.toFixed(1)}% → ${currCpuAvg.toFixed(1)}%)` - anomalies.push({ target_id: server.target_id, server_name: server.server_name, @@ -101,19 +82,10 @@ export default defineEventHandler(async (event) => { change_rate: cpuChange, direction: cpuChange >= 0 ? 'up' : 'down' }) - - // 중복 아니면 로그 저장 - if (!recentLogExists.get(server.target_id, 'CPU')) { - insertLog.run(server.target_id, server.server_name, 'CPU', level, currCpuAvg, cpuChange, message) - } } - // Memory 이상 감지 + 로그 저장 + // Memory 이상 감지 if (memChange !== null && Math.abs(memChange) >= THRESHOLD) { - const level = Math.abs(memChange) >= 100 ? 'danger' : 'warning' - const direction = memChange >= 0 ? '증가' : '감소' - const message = `Memory ${direction} 감지 (${prevMemAvg.toFixed(1)}% → ${currMemAvg.toFixed(1)}%)` - anomalies.push({ target_id: server.target_id, server_name: server.server_name, @@ -123,10 +95,6 @@ export default defineEventHandler(async (event) => { change_rate: memChange, direction: memChange >= 0 ? 'up' : 'down' }) - - if (!recentLogExists.get(server.target_id, 'Memory')) { - insertLog.run(server.target_id, server.server_name, 'Memory', level, currMemAvg, memChange, message) - } } } diff --git a/backend/api/anomaly/trend.get.ts b/backend/api/anomaly/trend.get.ts index 5e63206..3c461e7 100644 --- a/backend/api/anomaly/trend.get.ts +++ b/backend/api/anomaly/trend.get.ts @@ -1,44 +1,29 @@ -import { getDb } from '../../utils/db' +import { query } 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 SLOPE_THRESHOLD = 0.5 + const MIN_SAMPLES = 10 + const WINDOW_MINUTES = 30 - const servers = db.prepare(` - SELECT target_id, server_name + const servers = await query(` + SELECT target_id, name as server_name FROM server_targets WHERE is_active = 1 - ORDER BY server_name - `).all() as any[] + ORDER BY name + `) 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[] + const snapshots = await query(` + SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at, + EXTRACT(EPOCH FROM (NOW() - checked_at)) / 60 as minutes_ago + FROM server_logs + WHERE target_id = $1 AND is_success = 1 + AND checked_at >= NOW() - INTERVAL '${WINDOW_MINUTES} minutes' + ORDER BY checked_at ASC + `, [server.target_id]) if (snapshots.length < MIN_SAMPLES) { serverResults.push({ @@ -56,33 +41,28 @@ export default defineEventHandler(async (event) => { 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 + const currCpu = Number(current.cpu_percent) || 0 + const currMem = Number(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 })) + const cpuPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(s.cpu_percent) || 0 })) + const memPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(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 + let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 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) { @@ -98,14 +78,11 @@ export default defineEventHandler(async (event) => { const cpuReg = linearRegression(cpuPoints) const memReg = linearRegression(memPoints) - // 분당 기울기로 환산 (수집 간격 고려) - const totalMinutes = WINDOW_MINUTES - const cpuSlopePerMin = (cpuReg.slope * n) / totalMinutes - const memSlopePerMin = (memReg.slope * n) / totalMinutes + const cpuSlopePerMin = (cpuReg.slope * n) / WINDOW_MINUTES + const memSlopePerMin = (memReg.slope * n) / WINDOW_MINUTES - // 추세 판단 function getTrend(slope: number, r2: number): string { - if (r2 < 0.3) return 'unstable' // 추세가 불안정 + if (r2 < 0.3) return 'unstable' if (slope >= SLOPE_THRESHOLD) return 'rising' if (slope <= -SLOPE_THRESHOLD) return 'falling' return 'stable' @@ -114,10 +91,9 @@ export default defineEventHandler(async (event) => { 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% 이상 + if (cpuSlopePerMin >= 1.0 || memSlopePerMin >= 1.0) status = 'danger' serverResults.push({ target_id: server.target_id, @@ -134,11 +110,8 @@ export default defineEventHandler(async (event) => { 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, @@ -149,17 +122,10 @@ export default defineEventHandler(async (event) => { 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, @@ -170,10 +136,6 @@ export default defineEventHandler(async (event) => { trend: memTrend, level }) - - if (!recentLogExists.get(server.target_id, 'Memory')) { - insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memSlopePerMin, message) - } } } diff --git a/backend/api/anomaly/zscore.get.ts b/backend/api/anomaly/zscore.get.ts index 15110fa..1c742ba 100644 --- a/backend/api/anomaly/zscore.get.ts +++ b/backend/api/anomaly/zscore.get.ts @@ -1,41 +1,27 @@ -import { getDb } from '../../utils/db' +import { query } from '../../utils/db' export default defineEventHandler(async (event) => { - const db = getDb() const WARNING_Z = 2.0 const DANGER_Z = 3.0 - const servers = db.prepare(` - SELECT target_id, server_name + const servers = await query(` + SELECT target_id, name as server_name FROM server_targets WHERE is_active = 1 - ORDER BY server_name - `).all() as any[] + ORDER BY name + `) 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 (?, ?, 'zscore', ?, ?, ?, ?, ?) - `) - - const recentLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'zscore' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - for (const server of servers) { - const snapshots = db.prepare(` - SELECT cpu_percent, memory_percent, collected_at - FROM server_snapshots - WHERE target_id = ? - AND collected_at >= datetime('now', '-1 hour', 'localtime') - ORDER BY collected_at DESC - `).all(server.target_id) as any[] + const snapshots = await query(` + SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at + FROM server_logs + WHERE target_id = $1 + AND checked_at >= NOW() - INTERVAL '1 hour' + ORDER BY checked_at DESC + `, [server.target_id]) if (snapshots.length < 10) { serverResults.push({ @@ -54,17 +40,17 @@ export default defineEventHandler(async (event) => { } const current = snapshots[0] - const currCpu = current.cpu_percent ?? 0 - const currMem = current.memory_percent ?? 0 + const currCpu = Number(current.cpu_percent) || 0 + const currMem = Number(current.memory_percent) || 0 - const cpuValues = snapshots.map(s => s.cpu_percent ?? 0) - const memValues = snapshots.map(s => s.memory_percent ?? 0) + const cpuValues = snapshots.map((s: any) => Number(s.cpu_percent) || 0) + const memValues = snapshots.map((s: any) => Number(s.memory_percent) || 0) - const cpuAvg = cpuValues.reduce((a, b) => a + b, 0) / cpuValues.length - const memAvg = memValues.reduce((a, b) => a + b, 0) / memValues.length + const cpuAvg = cpuValues.reduce((a: number, b: number) => a + b, 0) / cpuValues.length + const memAvg = memValues.reduce((a: number, b: number) => a + b, 0) / memValues.length - const cpuVariance = cpuValues.reduce((sum, val) => sum + Math.pow(val - cpuAvg, 2), 0) / cpuValues.length - const memVariance = memValues.reduce((sum, val) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length + const cpuVariance = cpuValues.reduce((sum: number, val: number) => sum + Math.pow(val - cpuAvg, 2), 0) / cpuValues.length + const memVariance = memValues.reduce((sum: number, val: number) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length const cpuStd = Math.sqrt(cpuVariance) const memStd = Math.sqrt(memVariance) @@ -91,11 +77,9 @@ export default defineEventHandler(async (event) => { status }) - // CPU 이상 감지 + 로그 저장 + // CPU 이상 감지 if (Math.abs(cpuZscore) >= WARNING_Z) { const level = Math.abs(cpuZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = cpuZscore >= 0 ? '높음' : '낮음' - const message = `CPU 평균 대비 ${Math.abs(cpuZscore).toFixed(1)}σ ${direction} (평균: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` anomalies.push({ target_id: server.target_id, @@ -108,17 +92,11 @@ export default defineEventHandler(async (event) => { direction: cpuZscore >= 0 ? 'up' : 'down', level }) - - if (!recentLogExists.get(server.target_id, 'CPU')) { - insertLog.run(server.target_id, server.server_name, 'CPU', level, currCpu, cpuZscore, message) - } } - // Memory 이상 감지 + 로그 저장 + // Memory 이상 감지 if (Math.abs(memZscore) >= WARNING_Z) { const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = memZscore >= 0 ? '높음' : '낮음' - const message = `Memory 평균 대비 ${Math.abs(memZscore).toFixed(1)}σ ${direction} (평균: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` anomalies.push({ target_id: server.target_id, @@ -131,10 +109,6 @@ export default defineEventHandler(async (event) => { direction: memZscore >= 0 ? 'up' : 'down', level }) - - if (!recentLogExists.get(server.target_id, 'Memory')) { - insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memZscore, message) - } } }