시스템 모니터

This commit is contained in:
2025-12-28 12:03:48 +09:00
parent dbae6649bc
commit a871ec8008
73 changed files with 21354 additions and 1 deletions

View File

@@ -0,0 +1,178 @@
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()
}
})

View File

@@ -0,0 +1,80 @@
import { getDb } from '../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const query = getQuery(event)
const type = query.type as string || 'short-term'
const period = query.period as string || '24h'
// 기간/간격 계산
let intervalClause = ''
let groupFormat = ''
if (period === '1h') {
intervalClause = `'-1 hours'`
groupFormat = '%Y-%m-%d %H:%M' // 분 단위
} else if (period === '6h') {
intervalClause = `'-6 hours'`
groupFormat = '%Y-%m-%d %H:00' // 시간 단위
} else if (period === '12h') {
intervalClause = `'-12 hours'`
groupFormat = '%Y-%m-%d %H:00'
} else if (period === '24h') {
intervalClause = `'-24 hours'`
groupFormat = '%Y-%m-%d %H:00'
} else if (period === '7d') {
intervalClause = `'-7 days'`
groupFormat = '%Y-%m-%d' // 일 단위
} else if (period === '30d') {
intervalClause = `'-30 days'`
groupFormat = '%Y-%m-%d'
} else {
intervalClause = `'-24 hours'`
groupFormat = '%Y-%m-%d %H:00'
}
// 시간대별 집계
const rows = db.prepare(`
SELECT
strftime('${groupFormat}', detected_at) as time_slot,
SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning,
SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger
FROM anomaly_logs
WHERE detect_type = ?
AND detected_at >= datetime('now', ${intervalClause}, 'localtime')
GROUP BY time_slot
ORDER BY time_slot ASC
`).all(type) as any[]
// 시간 포맷 변환
const data = rows.map(r => ({
time: formatTimeLabel(r.time_slot, period),
warning: r.warning,
danger: r.danger
}))
return {
data,
type,
period,
timestamp: new Date().toISOString()
}
})
function formatTimeLabel(timeSlot: string, period: string): string {
if (!timeSlot) return ''
if (period === '7d' || period === '30d') {
// 일 단위: MM/DD
const parts = timeSlot.split('-')
return `${parts[1]}/${parts[2]}`
} else {
// 시간 단위: HH:MM
const parts = timeSlot.split(' ')
if (parts.length === 2) {
return parts[1].substring(0, 5)
}
return timeSlot.substring(11, 16)
}
}

View File

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

View File

@@ -0,0 +1,141 @@
import { getDb } from '../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const THRESHOLD = 30 // 30% 이상 변화 시 이상 감지
// 활성 서버 목록
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[] = []
// 로그 저장용 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) {
const snapshots = db.prepare(`
SELECT cpu_percent, memory_percent, collected_at
FROM server_snapshots
WHERE target_id = ?
ORDER BY collected_at DESC
LIMIT 20
`).all(server.target_id) as any[]
if (snapshots.length < 4) {
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_change: null,
mem_change: null,
status: 'normal'
})
continue
}
const half = Math.floor(snapshots.length / 2)
const currSnapshots = snapshots.slice(0, half)
const prevSnapshots = snapshots.slice(half)
const currCpuAvg = currSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / currSnapshots.length
const prevCpuAvg = prevSnapshots.reduce((sum, s) => sum + (s.cpu_percent || 0), 0) / prevSnapshots.length
const currMemAvg = currSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / currSnapshots.length
const prevMemAvg = prevSnapshots.reduce((sum, s) => sum + (s.memory_percent || 0), 0) / prevSnapshots.length
let cpuChange: number | null = null
let memChange: number | null = null
if (prevCpuAvg > 1) {
cpuChange = ((currCpuAvg - prevCpuAvg) / prevCpuAvg) * 100
} else {
cpuChange = currCpuAvg - prevCpuAvg
}
if (prevMemAvg > 1) {
memChange = ((currMemAvg - prevMemAvg) / prevMemAvg) * 100
} else {
memChange = currMemAvg - prevMemAvg
}
let status = 'normal'
const maxChange = Math.max(Math.abs(cpuChange || 0), Math.abs(memChange || 0))
if (maxChange >= 100) status = 'danger'
else if (maxChange >= THRESHOLD) status = 'warning'
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_change: cpuChange,
mem_change: memChange,
status
})
// CPU 이상 감지 + 로그 저장
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({
target_id: server.target_id,
server_name: server.server_name,
metric: 'CPU',
prev_avg: prevCpuAvg,
curr_avg: currCpuAvg,
change_rate: cpuChange,
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 이상 감지 + 로그 저장
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({
target_id: server.target_id,
server_name: server.server_name,
metric: 'Memory',
prev_avg: prevMemAvg,
curr_avg: currMemAvg,
change_rate: memChange,
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)
}
}
}
anomalies.sort((a, b) => Math.abs(b.change_rate) - Math.abs(a.change_rate))
return {
anomalies,
servers: serverResults,
threshold: THRESHOLD,
timestamp: new Date().toISOString()
}
})

View File

@@ -0,0 +1,192 @@
import { getDb } from '../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const SLOPE_THRESHOLD = 0.5 // 분당 0.5% 이상 증가/감소 시 이상
const MIN_SAMPLES = 10 // 최소 10개 샘플 필요
const WINDOW_MINUTES = 30 // 30분 윈도우
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 (?, ?, '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) {
// 최근 30분 데이터 조회
const snapshots = db.prepare(`
SELECT cpu_percent, memory_percent, collected_at,
(julianday('now', 'localtime') - julianday(collected_at)) * 24 * 60 as minutes_ago
FROM server_snapshots
WHERE target_id = ? AND is_online = 1
AND collected_at >= datetime('now', '-${WINDOW_MINUTES} minutes', 'localtime')
ORDER BY collected_at ASC
`).all(server.target_id) as any[]
if (snapshots.length < MIN_SAMPLES) {
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].cpu_percent : null,
mem_current: snapshots.length > 0 ? snapshots[snapshots.length - 1].memory_percent : null,
cpu_slope: null,
mem_slope: null,
cpu_trend: null,
mem_trend: null,
sample_count: snapshots.length,
status: 'insufficient'
})
continue
}
// 선형 회귀 계산 (최소제곱법)
// y = ax + b, a = slope (기울기)
const n = snapshots.length
const current = snapshots[n - 1]
const currCpu = current.cpu_percent ?? 0
const currMem = current.memory_percent ?? 0
// x = 시간 (분), y = 값
const cpuPoints = snapshots.map((s, i) => ({ x: i, y: s.cpu_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 } {
const n = points.length
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0
for (const p of points) {
sumX += p.x
sumY += p.y
sumXY += p.x * p.y
sumX2 += p.x * p.x
sumY2 += p.y * p.y
}
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
const intercept = (sumY - slope * sumX) / n
// R² (결정계수) 계산
const yMean = sumY / n
let ssTotal = 0, ssResidual = 0
for (const p of points) {
const yPred = slope * p.x + intercept
ssTotal += Math.pow(p.y - yMean, 2)
ssResidual += Math.pow(p.y - yPred, 2)
}
const r2 = ssTotal > 0 ? 1 - (ssResidual / ssTotal) : 0
return { slope, intercept, r2 }
}
const cpuReg = linearRegression(cpuPoints)
const memReg = linearRegression(memPoints)
// 분당 기울기로 환산 (수집 간격 고려)
const totalMinutes = WINDOW_MINUTES
const cpuSlopePerMin = (cpuReg.slope * n) / totalMinutes
const memSlopePerMin = (memReg.slope * n) / totalMinutes
// 추세 판단
function getTrend(slope: number, r2: number): string {
if (r2 < 0.3) return 'unstable' // 추세가 불안정
if (slope >= SLOPE_THRESHOLD) return 'rising'
if (slope <= -SLOPE_THRESHOLD) return 'falling'
return 'stable'
}
const cpuTrend = getTrend(cpuSlopePerMin, cpuReg.r2)
const memTrend = getTrend(memSlopePerMin, memReg.r2)
// 상태 결정
let status = 'normal'
if (cpuTrend === 'rising' || memTrend === 'rising') status = 'warning'
if (cpuSlopePerMin >= 1.0 || memSlopePerMin >= 1.0) status = 'danger' // 분당 1% 이상
serverResults.push({
target_id: server.target_id,
server_name: server.server_name,
cpu_current: currCpu,
mem_current: currMem,
cpu_slope: cpuSlopePerMin,
mem_slope: memSlopePerMin,
cpu_trend: cpuTrend,
mem_trend: memTrend,
cpu_r2: cpuReg.r2,
mem_r2: memReg.r2,
sample_count: snapshots.length,
status
})
// CPU 이상감지 + 로그 저장
if (cpuTrend === 'rising' && cpuReg.r2 >= 0.3) {
const level = cpuSlopePerMin >= 1.0 ? 'danger' : 'warning'
const message = `CPU 지속 상승 중 (분당 +${cpuSlopePerMin.toFixed(2)}%, R²=${cpuReg.r2.toFixed(2)})`
anomalies.push({
target_id: server.target_id,
server_name: server.server_name,
metric: 'CPU',
current: currCpu,
slope: cpuSlopePerMin,
r2: cpuReg.r2,
trend: cpuTrend,
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) {
const level = memSlopePerMin >= 1.0 ? 'danger' : 'warning'
const message = `Memory 지속 상승 중 (분당 +${memSlopePerMin.toFixed(2)}%, R²=${memReg.r2.toFixed(2)})`
anomalies.push({
target_id: server.target_id,
server_name: server.server_name,
metric: 'Memory',
current: currMem,
slope: memSlopePerMin,
r2: memReg.r2,
trend: memTrend,
level
})
if (!recentLogExists.get(server.target_id, 'Memory')) {
insertLog.run(server.target_id, server.server_name, 'Memory', level, currMem, memSlopePerMin, message)
}
}
}
anomalies.sort((a, b) => b.slope - a.slope)
return {
anomalies,
servers: serverResults,
config: {
slope_threshold: SLOPE_THRESHOLD,
window_minutes: WINDOW_MINUTES,
min_samples: MIN_SAMPLES
},
timestamp: new Date().toISOString()
}
})

View File

@@ -0,0 +1,149 @@
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()
}
})

View File

@@ -0,0 +1,78 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { year, month, week } = query as { year?: string, month?: string, week?: string }
if (!year || !month || !week) {
return { error: 'year, month, week are required' }
}
// 해당 월의 첫날과 마지막날
const y = parseInt(year)
const m = parseInt(month)
const w = parseInt(week)
// 해당 월의 첫날
const firstDayOfMonth = new Date(y, m - 1, 1)
const firstDayWeekday = firstDayOfMonth.getDay() // 0=일, 1=월, ...
// 주차의 시작일 계산 (월요일 기준)
// 1주차: 1일이 포함된 주
const mondayOffset = firstDayWeekday === 0 ? -6 : 1 - firstDayWeekday
const firstMondayOfMonth = new Date(y, m - 1, 1 + mondayOffset)
// 선택한 주차의 월요일
const weekStart = new Date(firstMondayOfMonth)
weekStart.setDate(weekStart.getDate() + (w - 1) * 7)
// 주차의 일요일
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekEnd.getDate() + 6)
// 해당 주의 날짜 목록 생성
const weekDates: string[] = []
for (let i = 0; i < 7; i++) {
const d = new Date(weekStart)
d.setDate(d.getDate() + i)
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
weekDates.push(dateStr)
}
const db = getDb()
// 시작/종료 날짜
const startDate = weekDates[0]
const endDate = weekDates[6]
// 1시간 단위 성공률 조회
const heatmapData = db.prepare(`
SELECT
date(checked_at) as date,
strftime('%H', checked_at) || ':00' as time_slot,
COUNT(*) as total_count,
SUM(CASE WHEN is_success = 1 THEN 1 ELSE 0 END) as success_count,
ROUND(SUM(CASE WHEN is_success = 1 THEN 1.0 ELSE 0.0 END) / COUNT(*) * 100, 1) as success_rate
FROM privnet_logs
WHERE date(checked_at) >= ? AND date(checked_at) <= ?
GROUP BY date, time_slot
ORDER BY date, time_slot
`).all(startDate, endDate)
// 해당 월의 주차 수 계산
const lastDayOfMonth = new Date(y, m, 0)
const lastDate = lastDayOfMonth.getDate()
// 마지막 날이 몇 주차인지 계산
const lastDayFromFirstMonday = Math.floor((lastDayOfMonth.getTime() - firstMondayOfMonth.getTime()) / (1000 * 60 * 60 * 24))
const totalWeeks = Math.ceil((lastDayFromFirstMonday + 1) / 7)
return {
heatmapData,
weekDates,
totalWeeks: Math.max(totalWeeks, 1),
year: y,
month: m,
week: w
}
})

View File

@@ -0,0 +1,33 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { year, month, day, hour } = query as {
year?: string, month?: string, day?: string, hour?: string
}
if (!year || !month || !day || !hour) {
return { error: 'year, month, day, hour are required' }
}
const db = getDb()
// 해당 시간대 로그 조회
const startTime = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hour.padStart(2, '0')}:00:00`
const endTime = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hour.padStart(2, '0')}:59:59`
const logs = db.prepare(`
SELECT
l.id,
l.checked_at,
l.is_success,
t.name as target_name,
t.url as target_url
FROM privnet_logs l
JOIN privnet_targets t ON l.target_id = t.id
WHERE l.checked_at >= ? AND l.checked_at <= ?
ORDER BY l.checked_at DESC
`).all(startTime, endTime)
return { logs }
})

View File

@@ -0,0 +1,11 @@
import { privnetScheduler } from '../../../../utils/privnet-scheduler'
export default defineEventHandler(() => {
privnetScheduler.start()
return {
success: true,
message: 'Privnet scheduler started',
isRunning: privnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,11 @@
import { privnetScheduler } from '../../../../utils/privnet-scheduler'
export default defineEventHandler(() => {
privnetScheduler.stop()
return {
success: true,
message: 'Privnet scheduler stopped',
isRunning: privnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,41 @@
import { getDb } from '../../../utils/db'
import { privnetScheduler } from '../../../utils/privnet-scheduler'
export default defineEventHandler(() => {
const db = getDb()
// 현재 상태 조회
const status = db.prepare(`
SELECT
ps.*,
pt.name as last_target_name,
pt.url as last_target_url
FROM privnet_status ps
LEFT JOIN privnet_targets pt ON ps.last_target_id = pt.id
WHERE ps.id = 1
`).get()
// 최근 10개 로그
const recentLogs = db.prepare(`
SELECT
pl.*,
pt.name as target_name,
pt.url as target_url
FROM privnet_logs pl
JOIN privnet_targets pt ON pl.target_id = pt.id
ORDER BY pl.checked_at DESC
LIMIT 10
`).all()
// 활성 타겟 수
const targetCount = db.prepare(`
SELECT COUNT(*) as cnt FROM privnet_targets WHERE is_active = 1
`).get() as { cnt: number }
return {
status,
recentLogs,
targetCount: targetCount.cnt,
schedulerRunning: privnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,10 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler((event) => {
const db = getDb()
const id = getRouterParam(event, 'id')
db.prepare(`DELETE FROM privnet_targets WHERE id = ?`).run(id)
return { success: true }
})

View File

@@ -0,0 +1,29 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const { name, url, is_active } = body
if (!name || !url) {
throw createError({
statusCode: 400,
message: 'name과 url은 필수입니다'
})
}
db.prepare(`
UPDATE privnet_targets
SET name = ?, url = ?, is_active = ?, updated_at = datetime('now', 'localtime')
WHERE id = ?
`).run(name, url, is_active ? 1 : 0, id)
return {
id: Number(id),
name,
url,
is_active: is_active ? 1 : 0
}
})

View File

@@ -0,0 +1,12 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(() => {
const db = getDb()
const targets = db.prepare(`
SELECT * FROM privnet_targets
ORDER BY id ASC
`).all()
return targets
})

View File

@@ -0,0 +1,27 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const body = await readBody(event)
const { name, url, is_active } = body
if (!name || !url) {
throw createError({
statusCode: 400,
message: 'name과 url은 필수입니다'
})
}
const result = db.prepare(`
INSERT INTO privnet_targets (name, url, is_active)
VALUES (?, ?, ?)
`).run(name, url, is_active ? 1 : 0)
return {
id: result.lastInsertRowid,
name,
url,
is_active: is_active ? 1 : 0
}
})

View File

@@ -0,0 +1,78 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { year, month, week } = query as { year?: string, month?: string, week?: string }
if (!year || !month || !week) {
return { error: 'year, month, week are required' }
}
// 해당 월의 첫날과 마지막날
const y = parseInt(year)
const m = parseInt(month)
const w = parseInt(week)
// 해당 월의 첫날
const firstDayOfMonth = new Date(y, m - 1, 1)
const firstDayWeekday = firstDayOfMonth.getDay() // 0=일, 1=월, ...
// 주차의 시작일 계산 (월요일 기준)
// 1주차: 1일이 포함된 주
const mondayOffset = firstDayWeekday === 0 ? -6 : 1 - firstDayWeekday
const firstMondayOfMonth = new Date(y, m - 1, 1 + mondayOffset)
// 선택한 주차의 월요일
const weekStart = new Date(firstMondayOfMonth)
weekStart.setDate(weekStart.getDate() + (w - 1) * 7)
// 주차의 일요일
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekEnd.getDate() + 6)
// 해당 주의 날짜 목록 생성
const weekDates: string[] = []
for (let i = 0; i < 7; i++) {
const d = new Date(weekStart)
d.setDate(d.getDate() + i)
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
weekDates.push(dateStr)
}
const db = getDb()
// 시작/종료 날짜
const startDate = weekDates[0]
const endDate = weekDates[6]
// 1시간 단위 성공률 조회
const heatmapData = db.prepare(`
SELECT
date(checked_at) as date,
strftime('%H', checked_at) || ':00' as time_slot,
COUNT(*) as total_count,
SUM(CASE WHEN is_success = 1 THEN 1 ELSE 0 END) as success_count,
ROUND(SUM(CASE WHEN is_success = 1 THEN 1.0 ELSE 0.0 END) / COUNT(*) * 100, 1) as success_rate
FROM pubnet_logs
WHERE date(checked_at) >= ? AND date(checked_at) <= ?
GROUP BY date, time_slot
ORDER BY date, time_slot
`).all(startDate, endDate)
// 해당 월의 주차 수 계산
const lastDayOfMonth = new Date(y, m, 0)
const lastDate = lastDayOfMonth.getDate()
// 마지막 날이 몇 주차인지 계산
const lastDayFromFirstMonday = Math.floor((lastDayOfMonth.getTime() - firstMondayOfMonth.getTime()) / (1000 * 60 * 60 * 24))
const totalWeeks = Math.ceil((lastDayFromFirstMonday + 1) / 7)
return {
heatmapData,
weekDates,
totalWeeks: Math.max(totalWeeks, 1),
year: y,
month: m,
week: w
}
})

View File

@@ -0,0 +1,33 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { year, month, day, hour } = query as {
year?: string, month?: string, day?: string, hour?: string
}
if (!year || !month || !day || !hour) {
return { error: 'year, month, day, hour are required' }
}
const db = getDb()
// 해당 시간대 로그 조회
const startTime = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hour.padStart(2, '0')}:00:00`
const endTime = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hour.padStart(2, '0')}:59:59`
const logs = db.prepare(`
SELECT
l.id,
l.checked_at,
l.is_success,
t.name as target_name,
t.url as target_url
FROM pubnet_logs l
JOIN pubnet_targets t ON l.target_id = t.id
WHERE l.checked_at >= ? AND l.checked_at <= ?
ORDER BY l.checked_at DESC
`).all(startTime, endTime)
return { logs }
})

View File

@@ -0,0 +1,11 @@
import { pubnetScheduler } from '../../../../utils/pubnet-scheduler'
export default defineEventHandler(() => {
pubnetScheduler.start()
return {
success: true,
message: 'Pubnet scheduler started',
isRunning: pubnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,11 @@
import { pubnetScheduler } from '../../../../utils/pubnet-scheduler'
export default defineEventHandler(() => {
pubnetScheduler.stop()
return {
success: true,
message: 'Pubnet scheduler stopped',
isRunning: pubnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,41 @@
import { getDb } from '../../../utils/db'
import { pubnetScheduler } from '../../../utils/pubnet-scheduler'
export default defineEventHandler(() => {
const db = getDb()
// 현재 상태 조회
const status = db.prepare(`
SELECT
ps.*,
pt.name as last_target_name,
pt.url as last_target_url
FROM pubnet_status ps
LEFT JOIN pubnet_targets pt ON ps.last_target_id = pt.id
WHERE ps.id = 1
`).get()
// 최근 10개 로그
const recentLogs = db.prepare(`
SELECT
pl.*,
pt.name as target_name,
pt.url as target_url
FROM pubnet_logs pl
JOIN pubnet_targets pt ON pl.target_id = pt.id
ORDER BY pl.checked_at DESC
LIMIT 10
`).all()
// 활성 타겟 수
const targetCount = db.prepare(`
SELECT COUNT(*) as cnt FROM pubnet_targets WHERE is_active = 1
`).get() as { cnt: number }
return {
status,
recentLogs,
targetCount: targetCount.cnt,
schedulerRunning: pubnetScheduler.getIsRunning()
}
})

View File

@@ -0,0 +1,10 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler((event) => {
const db = getDb()
const id = getRouterParam(event, 'id')
db.prepare(`DELETE FROM pubnet_targets WHERE id = ?`).run(id)
return { success: true }
})

View File

@@ -0,0 +1,29 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const { name, url, is_active } = body
if (!name || !url) {
throw createError({
statusCode: 400,
message: 'name과 url은 필수입니다'
})
}
db.prepare(`
UPDATE pubnet_targets
SET name = ?, url = ?, is_active = ?, updated_at = datetime('now', 'localtime')
WHERE id = ?
`).run(name, url, is_active ? 1 : 0, id)
return {
id: Number(id),
name,
url,
is_active: is_active ? 1 : 0
}
})

View File

@@ -0,0 +1,12 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(() => {
const db = getDb()
const targets = db.prepare(`
SELECT * FROM pubnet_targets
ORDER BY id ASC
`).all()
return targets
})

View File

@@ -0,0 +1,27 @@
import { getDb } from '../../../../utils/db'
export default defineEventHandler(async (event) => {
const db = getDb()
const body = await readBody(event)
const { name, url, is_active } = body
if (!name || !url) {
throw createError({
statusCode: 400,
message: 'name과 url은 필수입니다'
})
}
const result = db.prepare(`
INSERT INTO pubnet_targets (name, url, is_active)
VALUES (?, ?, ?)
`).run(name, url, is_active ? 1 : 0)
return {
id: result.lastInsertRowid,
name,
url,
is_active: is_active ? 1 : 0
}
})

View File

@@ -0,0 +1,30 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const db = getDb()
// 최신 수집 시간 기준 컨테이너 목록
const containers = db.prepare(`
SELECT DISTINCT container_name
FROM server_containers
WHERE target_id = ?
AND collected_at = (
SELECT MAX(collected_at)
FROM server_containers
WHERE target_id = ?
)
ORDER BY container_name ASC
`).all(targetId, targetId)
return containers.map((c: any) => c.container_name)
})

View File

@@ -0,0 +1,57 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
const period = (query.period as string) || '1h'
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const periodMap: Record<string, string> = {
'1h': '-1 hour',
'2h': '-2 hours',
'3h': '-3 hours',
'4h': '-4 hours',
'5h': '-5 hours',
'6h': '-6 hours',
'12h': '-12 hours',
'18h': '-18 hours',
'24h': '-24 hours',
'7d': '-7 days',
'30d': '-30 days'
}
const timeOffset = periodMap[period] || '-1 hour'
const db = getDb()
const containers = db.prepare(`
SELECT
container_id,
container_name,
container_status,
cpu_percent,
memory_usage,
memory_limit,
memory_percent,
uptime,
network_rx,
network_tx,
collected_at
FROM server_containers
WHERE target_id = ?
AND collected_at >= datetime('now', 'localtime', ?)
ORDER BY collected_at ASC, container_name ASC
`).all(targetId, timeOffset)
return {
target_id: targetId,
period,
data: containers
}
})

View File

@@ -0,0 +1,29 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const db = getDb()
// 최신 수집 시간 기준 디스크 목록 (물리 디스크만)
const disks = db.prepare(`
SELECT DISTINCT device_name, mount_point, fs_type, disk_total, disk_used, disk_percent
FROM server_disks
WHERE target_id = ?
AND collected_at = (SELECT MAX(collected_at) FROM server_disks WHERE target_id = ?)
AND device_name NOT LIKE '%loop%'
AND mount_point NOT LIKE '%/snap%'
AND fs_type NOT IN ('tmpfs', 'squashfs', 'overlay')
ORDER BY mount_point ASC
`).all(targetId, targetId)
return disks
})

View File

@@ -0,0 +1,54 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
const period = (query.period as string) || '1h'
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const periodMap: Record<string, string> = {
'1h': '-1 hour',
'2h': '-2 hours',
'3h': '-3 hours',
'4h': '-4 hours',
'5h': '-5 hours',
'6h': '-6 hours',
'12h': '-12 hours',
'18h': '-18 hours',
'24h': '-24 hours',
'7d': '-7 days',
'30d': '-30 days'
}
const timeOffset = periodMap[period] || '-1 hour'
const db = getDb()
const disks = db.prepare(`
SELECT
disk_id,
device_name,
mount_point,
fs_type,
disk_total,
disk_used,
disk_percent,
collected_at
FROM server_disks
WHERE target_id = ?
AND collected_at >= datetime('now', 'localtime', ?)
ORDER BY collected_at ASC, mount_point ASC
`).all(targetId, timeOffset)
return {
target_id: targetId,
period,
data: disks
}
})

View File

@@ -0,0 +1,32 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const db = getDb()
// 최신 스냅샷
const snapshot = db.prepare(`
SELECT
s.*,
t.server_name,
t.server_ip,
t.glances_url,
t.collect_interval
FROM server_snapshots s
JOIN server_targets t ON s.target_id = t.target_id
WHERE s.target_id = ?
ORDER BY s.collected_at DESC
LIMIT 1
`).get(targetId)
return snapshot || null
})

View File

@@ -0,0 +1,54 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
const period = (query.period as string) || '1h'
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const periodMap: Record<string, string> = {
'1h': '-1 hour',
'2h': '-2 hours',
'3h': '-3 hours',
'4h': '-4 hours',
'5h': '-5 hours',
'6h': '-6 hours',
'12h': '-12 hours',
'18h': '-18 hours',
'24h': '-24 hours',
'7d': '-7 days',
'30d': '-30 days'
}
const timeOffset = periodMap[period] || '-1 hour'
const db = getDb()
const networks = db.prepare(`
SELECT
network_id,
interface_name,
bytes_recv,
bytes_sent,
speed_recv,
speed_sent,
is_up,
collected_at
FROM server_networks
WHERE target_id = ?
AND collected_at >= datetime('now', 'localtime', ?)
ORDER BY collected_at ASC, interface_name ASC
`).all(targetId, timeOffset)
return {
target_id: targetId,
period,
data: networks
}
})

View File

@@ -0,0 +1,59 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler((event) => {
const query = getQuery(event)
const targetId = query.target_id as string
const period = (query.period as string) || '1h'
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
// 기간별 시간 계산
const periodMap: Record<string, string> = {
'1h': '-1 hour',
'2h': '-2 hours',
'3h': '-3 hours',
'4h': '-4 hours',
'5h': '-5 hours',
'6h': '-6 hours',
'12h': '-12 hours',
'18h': '-18 hours',
'24h': '-24 hours',
'7d': '-7 days',
'30d': '-30 days'
}
const timeOffset = periodMap[period] || '-1 hour'
const db = getDb()
const snapshots = db.prepare(`
SELECT
snapshot_id,
cpu_percent,
cpu_temp,
load_percent,
memory_percent,
memory_used,
memory_total,
swap_percent,
swap_used,
swap_total,
is_online,
collected_at
FROM server_snapshots
WHERE target_id = ?
AND collected_at >= datetime('now', 'localtime', ?)
ORDER BY collected_at ASC
`).all(targetId, timeOffset)
return {
target_id: targetId,
period,
data: snapshots
}
})

View File

@@ -0,0 +1,6 @@
import { startServerScheduler } from '../../../utils/server-scheduler'
export default defineEventHandler(() => {
startServerScheduler()
return { success: true, message: 'Server scheduler started' }
})

View File

@@ -0,0 +1,6 @@
import { stopServerScheduler } from '../../../utils/server-scheduler'
export default defineEventHandler(() => {
stopServerScheduler()
return { success: true, message: 'Server scheduler stopped' }
})

View File

@@ -0,0 +1,5 @@
import { getServerSchedulerStatus } from '../../utils/server-scheduler'
export default defineEventHandler(() => {
return getServerSchedulerStatus()
})

View File

@@ -0,0 +1,27 @@
import { getDb } from '../../../utils/db'
import { refreshServerTimer } from '../../../utils/server-scheduler'
export default defineEventHandler(async (event) => {
const targetId = getRouterParam(event, 'id')
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
// 스케줄러에서 제거
refreshServerTimer(Number(targetId))
const db = getDb()
const result = db.prepare(`
DELETE FROM server_targets WHERE target_id = ?
`).run(targetId)
return {
success: true,
changes: result.changes
}
})

View File

@@ -0,0 +1,36 @@
import { getDb } from '../../../utils/db'
import { refreshServerTimer } from '../../../utils/server-scheduler'
export default defineEventHandler(async (event) => {
const targetId = getRouterParam(event, 'id')
const body = await readBody(event)
const { server_name, server_ip, glances_url, is_active, collect_interval } = body
if (!targetId) {
throw createError({
statusCode: 400,
message: 'target_id is required'
})
}
const db = getDb()
const result = db.prepare(`
UPDATE server_targets
SET server_name = ?,
server_ip = ?,
glances_url = ?,
is_active = ?,
collect_interval = ?,
updated_at = datetime('now', 'localtime')
WHERE target_id = ?
`).run(server_name, server_ip, glances_url, is_active ? 1 : 0, collect_interval || 60, targetId)
// 스케줄러에 반영
refreshServerTimer(Number(targetId))
return {
success: true,
changes: result.changes
}
})

View File

@@ -0,0 +1,12 @@
import { getDb } from '../../../utils/db'
export default defineEventHandler(() => {
const db = getDb()
const targets = db.prepare(`
SELECT * FROM server_targets
ORDER BY target_id ASC
`).all()
return targets
})

View File

@@ -0,0 +1,33 @@
import { getDb } from '../../../utils/db'
import { refreshServerTimer } from '../../../utils/server-scheduler'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { server_name, server_ip, glances_url, is_active = 1, collect_interval = 60 } = body
if (!server_name || !server_ip || !glances_url) {
throw createError({
statusCode: 400,
message: 'server_name, server_ip, glances_url are required'
})
}
const db = getDb()
const result = db.prepare(`
INSERT INTO server_targets (server_name, server_ip, glances_url, is_active, collect_interval)
VALUES (?, ?, ?, ?, ?)
`).run(server_name, server_ip, glances_url, is_active ? 1 : 0, collect_interval)
const targetId = result.lastInsertRowid as number
// 스케줄러에 반영
if (is_active) {
refreshServerTimer(targetId)
}
return {
success: true,
target_id: targetId
}
})

View File

@@ -0,0 +1,27 @@
import { getDb } from '../../utils/db'
export default defineEventHandler(() => {
const db = getDb()
const rows = db.prepare(`
SELECT category, metric, warning, critical, danger, updated_at
FROM thresholds
ORDER BY category, metric
`).all() as any[]
// 카테고리별로 그룹화
const result: Record<string, Record<string, { warning: number; critical: number; danger: number }>> = {}
for (const row of rows) {
if (!result[row.category]) {
result[row.category] = {}
}
result[row.category][row.metric] = {
warning: row.warning,
critical: row.critical,
danger: row.danger
}
}
return result
})

View File

@@ -0,0 +1,54 @@
import { getDb } from '../../utils/db'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!body || typeof body !== 'object') {
throw createError({
statusCode: 400,
message: 'Invalid request body'
})
}
const db = getDb()
const now = new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' }).replace('T', ' ')
const stmt = db.prepare(`
UPDATE thresholds
SET warning = ?, critical = ?, danger = ?, updated_at = ?
WHERE category = ? AND metric = ?
`)
let updated = 0
for (const [category, metrics] of Object.entries(body)) {
if (typeof metrics !== 'object') continue
for (const [metric, values] of Object.entries(metrics as Record<string, any>)) {
if (!values || typeof values !== 'object') continue
const { warning, critical, danger } = values
// 유효성 검사
if (typeof warning !== 'number' || typeof critical !== 'number' || typeof danger !== 'number') {
continue
}
if (warning < 0 || warning > 100 || critical < 0 || critical > 100 || danger < 0 || danger > 100) {
continue
}
if (warning >= critical || critical >= danger) {
throw createError({
statusCode: 400,
message: `Invalid thresholds for ${category}.${metric}: warning < critical < danger 순서여야 합니다.`
})
}
const result = stmt.run(warning, critical, danger, now, category, metric)
if (result.changes > 0) updated++
}
}
return { success: true, updated }
})