Docker 파일

This commit is contained in:
2025-12-28 14:06:32 +09:00
parent 7d30d2b5a9
commit 9d2a6638b5
6 changed files with 158 additions and 276 deletions

View File

@@ -1,15 +1,14 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb()
const DEVIATION_THRESHOLD = 2.0 const DEVIATION_THRESHOLD = 2.0
const servers = db.prepare(` const servers = await query<any>(`
SELECT target_id, server_name SELECT target_id, name as server_name
FROM server_targets FROM server_targets
WHERE is_active = 1 WHERE is_active = 1
ORDER BY server_name ORDER BY name
`).all() as any[] `)
const now = new Date() const now = new Date()
const currentHour = now.getHours() const currentHour = now.getHours()
@@ -20,41 +19,32 @@ export default defineEventHandler(async (event) => {
const anomalies: any[] = [] const anomalies: any[] = []
const serverResults: 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) { for (const server of servers) {
const historicalData = db.prepare(` // 최근 14일 동일 시간대 데이터
SELECT cpu_percent, memory_percent, collected_at const historicalData = await query<any>(`
FROM server_snapshots SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at
WHERE target_id = ? FROM server_logs
AND collected_at >= datetime('now', '-14 days', 'localtime') WHERE target_id = $1
AND strftime('%H', collected_at) = ? AND checked_at >= NOW() - INTERVAL '14 days'
AND EXTRACT(HOUR FROM checked_at) = $2
AND ( AND (
(? = 'weekend' AND strftime('%w', collected_at) IN ('0', '6')) ($3 = 'weekend' AND EXTRACT(DOW FROM checked_at) IN (0, 6))
OR 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 ORDER BY checked_at DESC
`).all(server.target_id, currentHour.toString().padStart(2, '0'), dayType, dayType) as any[] `, [server.target_id, currentHour, dayType])
const current = db.prepare(` // 현재 값
SELECT cpu_percent, memory_percent const currentRows = await query<any>(`
FROM server_snapshots SELECT cpu_usage as cpu_percent, memory_usage as memory_percent
WHERE target_id = ? FROM server_logs
ORDER BY collected_at DESC WHERE target_id = $1
ORDER BY checked_at DESC
LIMIT 1 LIMIT 1
`).get(server.target_id) as any `, [server.target_id])
const current = currentRows[0]
if (!current || historicalData.length < 5) { if (!current || historicalData.length < 5) {
serverResults.push({ serverResults.push({
@@ -74,17 +64,17 @@ export default defineEventHandler(async (event) => {
continue continue
} }
const currCpu = current.cpu_percent ?? 0 const currCpu = Number(current.cpu_percent) || 0
const currMem = current.memory_percent ?? 0 const currMem = Number(current.memory_percent) || 0
const cpuValues = historicalData.map(s => s.cpu_percent ?? 0) const cpuValues = historicalData.map((s: any) => Number(s.cpu_percent) || 0)
const memValues = historicalData.map(s => s.memory_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 cpuAvg = cpuValues.reduce((a: number, b: number) => a + b, 0) / cpuValues.length
const memAvg = memValues.reduce((a, b) => a + b, 0) / memValues.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 cpuVariance = cpuValues.reduce((sum: number, val: number) => 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 memVariance = memValues.reduce((sum: number, val: number) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length
const cpuStd = Math.sqrt(cpuVariance) const cpuStd = Math.sqrt(cpuVariance)
const memStd = Math.sqrt(memVariance) const memStd = Math.sqrt(memVariance)
@@ -111,12 +101,9 @@ export default defineEventHandler(async (event) => {
status status
}) })
// CPU 이상감지 + 로그 저장 // CPU 이상감지
if (Math.abs(cpuDeviation) >= DEVIATION_THRESHOLD) { if (Math.abs(cpuDeviation) >= DEVIATION_THRESHOLD) {
const level = Math.abs(cpuDeviation) >= 3.0 ? 'danger' : 'warning' 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
@@ -130,18 +117,11 @@ export default defineEventHandler(async (event) => {
hour: currentHour, hour: currentHour,
day_type: dayType 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) { if (Math.abs(memDeviation) >= DEVIATION_THRESHOLD) {
const level = Math.abs(memDeviation) >= 3.0 ? 'danger' : 'warning' 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
@@ -155,10 +135,6 @@ export default defineEventHandler(async (event) => {
hour: currentHour, hour: currentHour,
day_type: dayType day_type: dayType
}) })
if (!recentLogExists.get(server.target_id, 'Memory')) {
insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memDeviation, message)
}
} }
} }

View File

@@ -1,57 +1,59 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb() const queryParams = getQuery(event)
const query = getQuery(event)
const type = query.type as string || 'short-term' const type = queryParams.type as string || 'short-term'
const period = query.period as string || '24h' const period = queryParams.period as string || '24h'
// 기간/간격 계산 // 기간/간격 계산
let intervalClause = '' let interval = '24 hours'
let groupFormat = '' let groupFormat = 'YYYY-MM-DD HH24:00'
if (period === '1h') { if (period === '1h') {
intervalClause = `'-1 hours'` interval = '1 hour'
groupFormat = '%Y-%m-%d %H:%M' // 분 단위 groupFormat = 'YYYY-MM-DD HH24:MI'
} else if (period === '6h') { } else if (period === '6h') {
intervalClause = `'-6 hours'` interval = '6 hours'
groupFormat = '%Y-%m-%d %H:00' // 시간 단위 groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '12h') { } else if (period === '12h') {
intervalClause = `'-12 hours'` interval = '12 hours'
groupFormat = '%Y-%m-%d %H:00' groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '24h') { } else if (period === '24h') {
intervalClause = `'-24 hours'` interval = '24 hours'
groupFormat = '%Y-%m-%d %H:00' groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '7d') { } else if (period === '7d') {
intervalClause = `'-7 days'` interval = '7 days'
groupFormat = '%Y-%m-%d' // 일 단위 groupFormat = 'YYYY-MM-DD'
} else if (period === '30d') { } else if (period === '30d') {
intervalClause = `'-30 days'` interval = '30 days'
groupFormat = '%Y-%m-%d' groupFormat = 'YYYY-MM-DD'
} else {
intervalClause = `'-24 hours'`
groupFormat = '%Y-%m-%d %H:00'
} }
// 시간대별 집계 // 시간대별 집계 (anomaly_logs 테이블이 없으면 빈 배열 반환)
const rows = db.prepare(` let rows: any[] = []
SELECT try {
strftime('${groupFormat}', detected_at) as time_slot, rows = await query<any>(`
SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning, SELECT
SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger to_char(detected_at, '${groupFormat}') as time_slot,
FROM anomaly_logs SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning,
WHERE detect_type = ? SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger
AND detected_at >= datetime('now', ${intervalClause}, 'localtime') FROM anomaly_logs
GROUP BY time_slot WHERE detect_type = $1
ORDER BY time_slot ASC AND detected_at >= NOW() - INTERVAL '${interval}'
`).all(type) as any[] GROUP BY time_slot
ORDER BY time_slot ASC
`, [type])
} catch (e) {
// 테이블이 없으면 빈 배열
rows = []
}
// 시간 포맷 변환 // 시간 포맷 변환
const data = rows.map(r => ({ const data = rows.map(r => ({
time: formatTimeLabel(r.time_slot, period), time: formatTimeLabel(r.time_slot, period),
warning: r.warning, warning: Number(r.warning),
danger: r.danger danger: Number(r.danger)
})) }))
return { return {
@@ -66,11 +68,9 @@ function formatTimeLabel(timeSlot: string, period: string): string {
if (!timeSlot) return '' if (!timeSlot) return ''
if (period === '7d' || period === '30d') { if (period === '7d' || period === '30d') {
// 일 단위: MM/DD
const parts = timeSlot.split('-') const parts = timeSlot.split('-')
return `${parts[1]}/${parts[2]}` return `${parts[1]}/${parts[2]}`
} else { } else {
// 시간 단위: HH:MM
const parts = timeSlot.split(' ') const parts = timeSlot.split(' ')
if (parts.length === 2) { if (parts.length === 2) {
return parts[1].substring(0, 5) return parts[1].substring(0, 5)

View File

@@ -1,33 +1,35 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb() const queryParams = getQuery(event)
const query = getQuery(event)
const type = query.type as string || 'short-term' const type = queryParams.type as string || 'short-term'
const period = query.period as string || '24h' const period = queryParams.period as string || '24h'
// 기간 계산 // 기간 계산
let intervalClause = '' let interval = '24 hours'
if (period.endsWith('h')) { if (period.endsWith('h')) {
const hours = parseInt(period) const hours = parseInt(period)
intervalClause = `'-${hours} hours'` interval = `${hours} hours`
} else if (period.endsWith('d')) { } else if (period.endsWith('d')) {
const days = parseInt(period) const days = parseInt(period)
intervalClause = `'-${days} days'` interval = `${days} days`
} else {
intervalClause = `'-24 hours'`
} }
const logs = db.prepare(` let logs: any[] = []
SELECT id, target_id, server_name, detect_type, metric, level, try {
current_value, threshold_value, message, detected_at logs = await query<any>(`
FROM anomaly_logs SELECT id, target_id, server_name, detect_type, metric, level,
WHERE detect_type = ? current_value, threshold_value, message, detected_at
AND detected_at >= datetime('now', ${intervalClause}, 'localtime') FROM anomaly_logs
ORDER BY detected_at DESC WHERE detect_type = $1
LIMIT 100 AND detected_at >= NOW() - INTERVAL '${interval}'
`).all(type) as any[] ORDER BY detected_at DESC
LIMIT 100
`, [type])
} catch (e) {
logs = []
}
return { return {
logs, logs,

View File

@@ -1,42 +1,27 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb()
const THRESHOLD = 30 // 30% 이상 변화 시 이상 감지 const THRESHOLD = 30 // 30% 이상 변화 시 이상 감지
// 활성 서버 목록 // 활성 서버 목록
const servers = db.prepare(` const servers = await query<any>(`
SELECT target_id, server_name SELECT target_id, name as server_name
FROM server_targets FROM server_targets
WHERE is_active = 1 WHERE is_active = 1
ORDER BY server_name ORDER BY name
`).all() as any[] `)
const anomalies: any[] = [] const anomalies: any[] = []
const serverResults: 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) { for (const server of servers) {
const snapshots = db.prepare(` const snapshots = await query<any>(`
SELECT cpu_percent, memory_percent, collected_at SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at
FROM server_snapshots FROM server_logs
WHERE target_id = ? WHERE target_id = $1
ORDER BY collected_at DESC ORDER BY checked_at DESC
LIMIT 20 LIMIT 20
`).all(server.target_id) as any[] `, [server.target_id])
if (snapshots.length < 4) { if (snapshots.length < 4) {
serverResults.push({ serverResults.push({
@@ -53,10 +38,10 @@ export default defineEventHandler(async (event) => {
const currSnapshots = snapshots.slice(0, half) const currSnapshots = snapshots.slice(0, half)
const prevSnapshots = snapshots.slice(half) const prevSnapshots = snapshots.slice(half)
const currCpuAvg = currSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / currSnapshots.length const currCpuAvg = currSnapshots.reduce((sum: number, s: any) => sum + (Number(s.cpu_percent) || 0), 0) / currSnapshots.length
const prevCpuAvg = prevSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / prevSnapshots.length const prevCpuAvg = prevSnapshots.reduce((sum: number, s: any) => sum + (Number(s.cpu_percent) || 0), 0) / prevSnapshots.length
const currMemAvg = currSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / currSnapshots.length const currMemAvg = currSnapshots.reduce((sum: number, s: any) => sum + (Number(s.memory_percent) || 0), 0) / currSnapshots.length
const prevMemAvg = prevSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / prevSnapshots.length const prevMemAvg = prevSnapshots.reduce((sum: number, s: any) => sum + (Number(s.memory_percent) || 0), 0) / prevSnapshots.length
let cpuChange: number | null = null let cpuChange: number | null = null
let memChange: number | null = null let memChange: number | null = null
@@ -86,12 +71,8 @@ export default defineEventHandler(async (event) => {
status status
}) })
// CPU 이상 감지 + 로그 저장 // CPU 이상 감지
if (cpuChange !== null && Math.abs(cpuChange) >= THRESHOLD) { 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
server_name: server.server_name, server_name: server.server_name,
@@ -101,19 +82,10 @@ export default defineEventHandler(async (event) => {
change_rate: cpuChange, change_rate: cpuChange,
direction: cpuChange >= 0 ? 'up' : 'down' 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) { 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
server_name: server.server_name, server_name: server.server_name,
@@ -123,10 +95,6 @@ export default defineEventHandler(async (event) => {
change_rate: memChange, change_rate: memChange,
direction: memChange >= 0 ? 'up' : 'down' 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)
}
} }
} }

View File

@@ -1,44 +1,29 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb() const SLOPE_THRESHOLD = 0.5
const SLOPE_THRESHOLD = 0.5 // 분당 0.5% 이상 증가/감소 시 이상 const MIN_SAMPLES = 10
const MIN_SAMPLES = 10 // 최소 10개 샘플 필요 const WINDOW_MINUTES = 30
const WINDOW_MINUTES = 30 // 30분 윈도우
const servers = db.prepare(` const servers = await query<any>(`
SELECT target_id, server_name SELECT target_id, name as server_name
FROM server_targets FROM server_targets
WHERE is_active = 1 WHERE is_active = 1
ORDER BY server_name ORDER BY name
`).all() as any[] `)
const anomalies: any[] = [] const anomalies: any[] = []
const serverResults: 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) { for (const server of servers) {
// 최근 30분 데이터 조회 const snapshots = await query<any>(`
const snapshots = db.prepare(` SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at,
SELECT cpu_percent, memory_percent, collected_at, EXTRACT(EPOCH FROM (NOW() - checked_at)) / 60 as minutes_ago
(julianday('now', 'localtime') - julianday(collected_at)) * 24 * 60 as minutes_ago FROM server_logs
FROM server_snapshots WHERE target_id = $1 AND is_success = 1
WHERE target_id = ? AND is_online = 1 AND checked_at >= NOW() - INTERVAL '${WINDOW_MINUTES} minutes'
AND collected_at >= datetime('now', '-${WINDOW_MINUTES} minutes', 'localtime') ORDER BY checked_at ASC
ORDER BY collected_at ASC `, [server.target_id])
`).all(server.target_id) as any[]
if (snapshots.length < MIN_SAMPLES) { if (snapshots.length < MIN_SAMPLES) {
serverResults.push({ serverResults.push({
@@ -56,33 +41,28 @@ export default defineEventHandler(async (event) => {
continue continue
} }
// 선형 회귀 계산 (최소제곱법)
// y = ax + b, a = slope (기울기)
const n = snapshots.length const n = snapshots.length
const current = snapshots[n - 1] const current = snapshots[n - 1]
const currCpu = current.cpu_percent ?? 0 const currCpu = Number(current.cpu_percent) || 0
const currMem = current.memory_percent ?? 0 const currMem = Number(current.memory_percent) || 0
// x = 시간 (분), y = 값 const cpuPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(s.cpu_percent) || 0 }))
const cpuPoints = snapshots.map((s, i) => ({ x: i, y: s.cpu_percent ?? 0 })) const memPoints = snapshots.map((s: any, i: number) => ({ x: i, y: Number(s.memory_percent) || 0 }))
const memPoints = snapshots.map((s, i) => ({ x: i, y: s.memory_percent ?? 0 }))
function linearRegression(points: { x: number, y: number }[]): { slope: number, intercept: number, r2: number } { function linearRegression(points: { x: number, y: number }[]): { slope: number, intercept: number, r2: number } {
const n = points.length 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) { for (const p of points) {
sumX += p.x sumX += p.x
sumY += p.y sumY += p.y
sumXY += p.x * p.y sumXY += p.x * p.y
sumX2 += p.x * p.x sumX2 += p.x * p.x
sumY2 += p.y * p.y
} }
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX) const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
const intercept = (sumY - slope * sumX) / n const intercept = (sumY - slope * sumX) / n
// R² (결정계수) 계산
const yMean = sumY / n const yMean = sumY / n
let ssTotal = 0, ssResidual = 0 let ssTotal = 0, ssResidual = 0
for (const p of points) { for (const p of points) {
@@ -98,14 +78,11 @@ export default defineEventHandler(async (event) => {
const cpuReg = linearRegression(cpuPoints) const cpuReg = linearRegression(cpuPoints)
const memReg = linearRegression(memPoints) const memReg = linearRegression(memPoints)
// 분당 기울기로 환산 (수집 간격 고려) const cpuSlopePerMin = (cpuReg.slope * n) / WINDOW_MINUTES
const totalMinutes = WINDOW_MINUTES const memSlopePerMin = (memReg.slope * n) / WINDOW_MINUTES
const cpuSlopePerMin = (cpuReg.slope * n) / totalMinutes
const memSlopePerMin = (memReg.slope * n) / totalMinutes
// 추세 판단
function getTrend(slope: number, r2: number): string { 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 'rising'
if (slope <= -SLOPE_THRESHOLD) return 'falling' if (slope <= -SLOPE_THRESHOLD) return 'falling'
return 'stable' return 'stable'
@@ -114,10 +91,9 @@ export default defineEventHandler(async (event) => {
const cpuTrend = getTrend(cpuSlopePerMin, cpuReg.r2) const cpuTrend = getTrend(cpuSlopePerMin, cpuReg.r2)
const memTrend = getTrend(memSlopePerMin, memReg.r2) const memTrend = getTrend(memSlopePerMin, memReg.r2)
// 상태 결정
let status = 'normal' let status = 'normal'
if (cpuTrend === 'rising' || memTrend === 'rising') status = 'warning' 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({ serverResults.push({
target_id: server.target_id, target_id: server.target_id,
@@ -134,11 +110,8 @@ export default defineEventHandler(async (event) => {
status status
}) })
// CPU 이상감지 + 로그 저장
if (cpuTrend === 'rising' && cpuReg.r2 >= 0.3) { if (cpuTrend === 'rising' && cpuReg.r2 >= 0.3) {
const level = cpuSlopePerMin >= 1.0 ? 'danger' : 'warning' const level = cpuSlopePerMin >= 1.0 ? 'danger' : 'warning'
const message = `CPU 지속 상승 중 (분당 +${cpuSlopePerMin.toFixed(2)}%, R²=${cpuReg.r2.toFixed(2)})`
anomalies.push({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
server_name: server.server_name, server_name: server.server_name,
@@ -149,17 +122,10 @@ export default defineEventHandler(async (event) => {
trend: cpuTrend, trend: cpuTrend,
level 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) { if (memTrend === 'rising' && memReg.r2 >= 0.3) {
const level = memSlopePerMin >= 1.0 ? 'danger' : 'warning' const level = memSlopePerMin >= 1.0 ? 'danger' : 'warning'
const message = `Memory 지속 상승 중 (분당 +${memSlopePerMin.toFixed(2)}%, R²=${memReg.r2.toFixed(2)})`
anomalies.push({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
server_name: server.server_name, server_name: server.server_name,
@@ -170,10 +136,6 @@ export default defineEventHandler(async (event) => {
trend: memTrend, trend: memTrend,
level level
}) })
if (!recentLogExists.get(server.target_id, 'Memory')) {
insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memSlopePerMin, message)
}
} }
} }

View File

@@ -1,41 +1,27 @@
import { getDb } from '../../utils/db' import { query } from '../../utils/db'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const db = getDb()
const WARNING_Z = 2.0 const WARNING_Z = 2.0
const DANGER_Z = 3.0 const DANGER_Z = 3.0
const servers = db.prepare(` const servers = await query<any>(`
SELECT target_id, server_name SELECT target_id, name as server_name
FROM server_targets FROM server_targets
WHERE is_active = 1 WHERE is_active = 1
ORDER BY server_name ORDER BY name
`).all() as any[] `)
const anomalies: any[] = [] const anomalies: any[] = []
const serverResults: 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) { for (const server of servers) {
const snapshots = db.prepare(` const snapshots = await query<any>(`
SELECT cpu_percent, memory_percent, collected_at SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, checked_at as collected_at
FROM server_snapshots FROM server_logs
WHERE target_id = ? WHERE target_id = $1
AND collected_at >= datetime('now', '-1 hour', 'localtime') AND checked_at >= NOW() - INTERVAL '1 hour'
ORDER BY collected_at DESC ORDER BY checked_at DESC
`).all(server.target_id) as any[] `, [server.target_id])
if (snapshots.length < 10) { if (snapshots.length < 10) {
serverResults.push({ serverResults.push({
@@ -54,17 +40,17 @@ export default defineEventHandler(async (event) => {
} }
const current = snapshots[0] const current = snapshots[0]
const currCpu = current.cpu_percent ?? 0 const currCpu = Number(current.cpu_percent) || 0
const currMem = current.memory_percent ?? 0 const currMem = Number(current.memory_percent) || 0
const cpuValues = snapshots.map(s => s.cpu_percent ?? 0) const cpuValues = snapshots.map((s: any) => Number(s.cpu_percent) || 0)
const memValues = snapshots.map(s => s.memory_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 cpuAvg = cpuValues.reduce((a: number, b: number) => a + b, 0) / cpuValues.length
const memAvg = memValues.reduce((a, b) => a + b, 0) / memValues.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 cpuVariance = cpuValues.reduce((sum: number, val: number) => 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 memVariance = memValues.reduce((sum: number, val: number) => sum + Math.pow(val - memAvg, 2), 0) / memValues.length
const cpuStd = Math.sqrt(cpuVariance) const cpuStd = Math.sqrt(cpuVariance)
const memStd = Math.sqrt(memVariance) const memStd = Math.sqrt(memVariance)
@@ -91,11 +77,9 @@ export default defineEventHandler(async (event) => {
status status
}) })
// CPU 이상 감지 + 로그 저장 // CPU 이상 감지
if (Math.abs(cpuZscore) >= WARNING_Z) { if (Math.abs(cpuZscore) >= WARNING_Z) {
const level = Math.abs(cpuZscore) >= DANGER_Z ? 'danger' : 'warning' 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
@@ -108,17 +92,11 @@ export default defineEventHandler(async (event) => {
direction: cpuZscore >= 0 ? 'up' : 'down', direction: cpuZscore >= 0 ? 'up' : 'down',
level 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) { if (Math.abs(memZscore) >= WARNING_Z) {
const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning' 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({ anomalies.push({
target_id: server.target_id, target_id: server.target_id,
@@ -131,10 +109,6 @@ export default defineEventHandler(async (event) => {
direction: memZscore >= 0 ? 'up' : 'down', direction: memZscore >= 0 ? 'up' : 'down',
level level
}) })
if (!recentLogExists.get(server.target_id, 'Memory')) {
insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memZscore, message)
}
} }
} }