Files
system-monitor/backend/api/anomaly/zscore.get.ts
2025-12-28 16:18:02 +09:00

124 lines
3.7 KiB
TypeScript

import { query } from '../../utils/db'
export default defineEventHandler(async (event) => {
const WARNING_Z = 2.0
const DANGER_Z = 3.0
const servers = await query<any>(`
SELECT target_id, server_name
FROM server_targets
WHERE is_active = 1
ORDER BY server_name
`)
const anomalies: any[] = []
const serverResults: any[] = []
for (const server of servers) {
const snapshots = await query<any>(`
SELECT cpu_percent, memory_percent, collected_at
FROM server_snapshots
WHERE target_id = $1
AND collected_at::timestamp >= NOW() - INTERVAL '1 hour'
ORDER BY collected_at DESC
`, [server.target_id])
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 = Number(current.cpu_percent) || 0
const currMem = Number(current.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: 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: 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)
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'
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
})
}
// Memory 이상 감지
if (Math.abs(memZscore) >= WARNING_Z) {
const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning'
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
})
}
}
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()
}
})