179 lines
6.2 KiB
TypeScript
179 lines
6.2 KiB
TypeScript
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()
|
||
}
|
||
})
|