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

150 lines
4.8 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 WARNING_Z = 2.0
const DANGER_Z = 3.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 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[]
if (snapshots.length < 10) {
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_zscore: null,
mem_zscore: null,
cpu_avg: null,
cpu_std: null,
mem_avg: null,
mem_std: null,
sample_count: snapshots.length,
status: 'insufficient'
})
continue
}
const current = snapshots[0]
const currCpu = current.cpu_percent ?? 0
const currMem = current.memory_percent ?? 0
const cpuValues = snapshots.map(s => s.cpu_percent ?? 0)
const memValues = snapshots.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 cpuZscore = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0
const memZscore = memStd > 0.1 ? (currMem - memAvg) / memStd : 0
let status = 'normal'
const maxZ = Math.max(Math.abs(cpuZscore), Math.abs(memZscore))
if (maxZ >= DANGER_Z) status = 'danger'
else if (maxZ >= WARNING_Z) status = 'warning'
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_current: currCpu,
mem_current: currMem,
cpu_zscore: cpuZscore,
mem_zscore: memZscore,
cpu_avg: cpuAvg,
cpu_std: cpuStd,
mem_avg: memAvg,
mem_std: memStd,
sample_count: snapshots.length,
status
})
// 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,
server_name: server.server_name,
metric: 'CPU',
current: currCpu,
avg: cpuAvg,
std: cpuStd,
zscore: cpuZscore,
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 이상 감지 + 로그 저장
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,
server_name: server.server_name,
metric: 'Memory',
current: currMem,
avg: memAvg,
std: memStd,
zscore: memZscore,
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)
}
}
}
anomalies.sort((a, b) => Math.abs(b.zscore) - Math.abs(a.zscore))
return {
anomalies,
servers: serverResults,
thresholds: { warning: WARNING_Z, danger: DANGER_Z },
timestamp: new Date().toISOString()
}
})