Files
system-monitor/backend/api/anomaly/baseline.get.ts
2025-12-28 12:03:48 +09:00

179 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getDb } 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
FROM server_targets
WHERE is_active = 1
ORDER BY server_name
`).all() as any[]
const now = new Date()
const currentHour = now.getHours()
const currentDayOfWeek = now.getDay()
const isWeekend = currentDayOfWeek === 0 || currentDayOfWeek === 6
const dayType = isWeekend ? 'weekend' : 'weekday'
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) = ?
AND (
(? = 'weekend' AND strftime('%w', collected_at) IN ('0', '6'))
OR
(? = 'weekday' AND strftime('%w', collected_at) NOT IN ('0', '6'))
)
ORDER BY collected_at DESC
`).all(server.target_id, currentHour.toString().padStart(2, '0'), dayType, dayType) as any[]
const current = db.prepare(`
SELECT cpu_percent, memory_percent
FROM server_snapshots
WHERE target_id = ?
ORDER BY collected_at DESC
LIMIT 1
`).get(server.target_id) as any
if (!current || historicalData.length < 5) {
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
current_hour: currentHour,
day_type: dayType,
cpu_current: current?.cpu_percent ?? null,
mem_current: current?.memory_percent ?? null,
cpu_baseline: null,
mem_baseline: null,
cpu_deviation: null,
mem_deviation: null,
sample_count: historicalData.length,
status: 'insufficient'
})
continue
}
const currCpu = current.cpu_percent ?? 0
const currMem = current.memory_percent ?? 0
const cpuValues = historicalData.map(s => s.cpu_percent ?? 0)
const memValues = historicalData.map(s => 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 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 cpuStd = Math.sqrt(cpuVariance)
const memStd = Math.sqrt(memVariance)
const cpuDeviation = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0
const memDeviation = memStd > 0.1 ? (currMem - memAvg) / memStd : 0
let status = 'normal'
const maxDev = Math.max(Math.abs(cpuDeviation), Math.abs(memDeviation))
if (maxDev >= 3.0) status = 'danger'
else if (maxDev >= DEVIATION_THRESHOLD) status = 'warning'
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
current_hour: currentHour,
day_type: dayType,
cpu_current: currCpu,
mem_current: currMem,
cpu_baseline: { avg: cpuAvg, std: cpuStd },
mem_baseline: { avg: memAvg, std: memStd },
cpu_deviation: cpuDeviation,
mem_deviation: memDeviation,
sample_count: historicalData.length,
status
})
// 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,
server_name: server.server_name,
metric: 'CPU',
current: currCpu,
baseline_avg: cpuAvg,
deviation: cpuDeviation,
direction: cpuDeviation >= 0 ? 'up' : 'down',
level,
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 이상감지 + 로그 저장
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,
server_name: server.server_name,
metric: 'Memory',
current: currMem,
baseline_avg: memAvg,
deviation: memDeviation,
direction: memDeviation >= 0 ? 'up' : 'down',
level,
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)
}
}
}
anomalies.sort((a, b) => Math.abs(b.deviation) - Math.abs(a.deviation))
return {
anomalies,
servers: serverResults,
context: {
current_hour: currentHour,
day_type: dayType,
day_type_label: isWeekend ? '주말' : '평일',
threshold: DEVIATION_THRESHOLD
},
timestamp: new Date().toISOString()
}
})