Files
system-monitor/backend/routes/_ws.ts
2025-12-28 12:03:48 +09:00

412 lines
13 KiB
TypeScript

import { getDb } from '../utils/db'
interface Client {
ws: any
interval: number
timer: ReturnType<typeof setInterval> | null
autoRefresh: boolean
}
const clients = new Map<any, Client>()
function getNetworkStatus() {
const db = getDb()
// pubnet 상태
const pubnetStatus = 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()
const pubnetLogs = 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 5
`).all()
// privnet 상태
const privnetStatus = 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()
const privnetLogs = 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 5
`).all()
return {
pubnet: {
status: pubnetStatus,
logs: pubnetLogs
},
privnet: {
status: privnetStatus,
logs: privnetLogs
},
timestamp: new Date().toISOString()
}
}
function getHistoricalData(datetime: string) {
const db = getDb()
// 특정 시간 이전의 로그 조회
const pubnetLogs = 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
WHERE pl.checked_at <= @datetime
ORDER BY pl.checked_at DESC
LIMIT 5
`).all({ datetime })
const privnetLogs = 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
WHERE pl.checked_at <= @datetime
ORDER BY pl.checked_at DESC
LIMIT 5
`).all({ datetime })
// 해당 시점의 최신 상태 (로그 기준)
const pubnetLatest = pubnetLogs[0] || null
const privnetLatest = privnetLogs[0] || null
return {
pubnet: {
status: pubnetLatest ? {
is_healthy: pubnetLatest.is_success,
last_checked_at: pubnetLatest.checked_at,
last_target_name: pubnetLatest.target_name
} : null,
logs: pubnetLogs
},
privnet: {
status: privnetLatest ? {
is_healthy: privnetLatest.is_success,
last_checked_at: privnetLatest.checked_at,
last_target_name: privnetLatest.target_name
} : null,
logs: privnetLogs
},
timestamp: datetime
}
}
// 임계값 조회
function getThresholds() {
const db = getDb()
const rows = db.prepare(`SELECT category, metric, warning, critical, danger FROM thresholds`).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
}
// 레벨 계산
function getLevel(value: number | null, threshold: { warning: number; critical: number; danger: number }): string {
if (value === null || value === undefined) return 'normal'
if (value >= threshold.danger) return 'danger'
if (value >= threshold.critical) return 'critical'
if (value >= threshold.warning) return 'warning'
return 'normal'
}
// 레벨 우선순위
const levelPriority: Record<string, number> = { normal: 0, warning: 1, critical: 2, danger: 3, offline: 4, stopped: 3 }
function getHighestLevel(levels: string[]): string {
let highest = 'normal'
for (const level of levels) {
if ((levelPriority[level] || 0) > (levelPriority[highest] || 0)) {
highest = level
}
}
return highest
}
// 서버 대시보드 데이터
function getServerDashboard() {
const db = getDb()
const thresholds = getThresholds()
const now = new Date()
const offlineThreshold = 5 * 60 * 1000 // 5분
// 서버 목록
const servers = db.prepare(`SELECT target_id, server_name, is_active FROM server_targets WHERE is_active = 1 ORDER BY server_name`).all() as any[]
const serverStatuses: any[] = []
const summaryServers = { total: servers.length, normal: 0, warning: 0, critical: 0, danger: 0, offline: 0 }
const summaryContainers = { total: 0, normal: 0, warning: 0, critical: 0, danger: 0, stopped: 0 }
for (const server of servers) {
// 최신 스냅샷
const snapshot = db.prepare(`
SELECT cpu_percent, memory_percent, collected_at
FROM server_snapshots
WHERE target_id = ?
ORDER BY collected_at DESC
LIMIT 1
`).get(server.target_id) as any
// 디스크 정보 조회 (루트 마운트 또는 최대 사용률)
const disk = db.prepare(`
SELECT disk_percent
FROM server_disks
WHERE target_id = ?
ORDER BY
CASE WHEN mount_point = '/' THEN 0 ELSE 1 END,
disk_percent DESC
LIMIT 1
`).get(server.target_id) as any
// 오프라인 체크
let isOffline = true
let lastCollected = null
if (snapshot && snapshot.collected_at) {
lastCollected = snapshot.collected_at
const collectedTime = new Date(snapshot.collected_at.replace(' ', 'T') + '+09:00').getTime()
isOffline = (now.getTime() - collectedTime) > offlineThreshold
}
// 서버 레벨 계산
let serverLevel = 'offline'
let cpuLevel = 'normal', memLevel = 'normal', diskLevel = 'normal'
if (!isOffline && snapshot) {
cpuLevel = getLevel(snapshot.cpu_percent, thresholds.server?.cpu || { warning: 70, critical: 85, danger: 95 })
memLevel = getLevel(snapshot.memory_percent, thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 })
diskLevel = getLevel(disk?.disk_percent ?? null, thresholds.server?.disk || { warning: 80, critical: 90, danger: 95 })
serverLevel = getHighestLevel([cpuLevel, memLevel, diskLevel])
}
// 컨테이너 조회 (최신 데이터, 중복 제거)
const containers = db.prepare(`
SELECT container_name, container_status, cpu_percent, memory_usage, memory_limit, uptime, network_rx, network_tx
FROM server_containers
WHERE target_id = ? AND collected_at = (
SELECT MAX(collected_at) FROM server_containers WHERE target_id = ?
)
GROUP BY container_name
ORDER BY container_name
`).all(server.target_id, server.target_id) as any[]
const containerStatuses: any[] = []
const containerSummary = { total: containers.length, normal: 0, warning: 0, critical: 0, danger: 0, stopped: 0 }
for (const c of containers) {
let containerLevel = 'normal'
if (c.container_status !== 'running') {
containerLevel = 'stopped'
containerSummary.stopped++
} else {
const cCpuLevel = getLevel(c.cpu_percent, thresholds.container?.cpu || { warning: 80, critical: 90, danger: 95 })
const cMemPercent = c.memory_limit ? (c.memory_usage / c.memory_limit * 100) : null
const cMemLevel = getLevel(cMemPercent, thresholds.container?.memory || { warning: 80, critical: 90, danger: 95 })
containerLevel = getHighestLevel([cCpuLevel, cMemLevel])
if (containerLevel === 'normal') containerSummary.normal++
else if (containerLevel === 'warning') containerSummary.warning++
else if (containerLevel === 'critical') containerSummary.critical++
else if (containerLevel === 'danger') containerSummary.danger++
}
containerStatuses.push({
name: c.container_name,
status: c.container_status,
level: containerLevel,
cpu_percent: c.cpu_percent,
memory_usage: c.memory_usage,
memory_limit: c.memory_limit,
uptime: c.uptime,
network_rx: c.network_rx,
network_tx: c.network_tx
})
}
// 요약 집계
summaryContainers.total += containerSummary.total
summaryContainers.normal += containerSummary.normal
summaryContainers.warning += containerSummary.warning
summaryContainers.critical += containerSummary.critical
summaryContainers.danger += containerSummary.danger
summaryContainers.stopped += containerSummary.stopped
if (serverLevel === 'offline') summaryServers.offline++
else if (serverLevel === 'danger') summaryServers.danger++
else if (serverLevel === 'critical') summaryServers.critical++
else if (serverLevel === 'warning') summaryServers.warning++
else summaryServers.normal++
serverStatuses.push({
target_id: server.target_id,
server_name: server.server_name,
level: serverLevel,
cpu_percent: snapshot?.cpu_percent ?? null,
cpu_level: cpuLevel,
memory_percent: snapshot?.memory_percent ?? null,
memory_level: memLevel,
disk_percent: disk?.disk_percent ?? null,
disk_level: diskLevel,
last_collected: lastCollected,
containers: containerStatuses,
container_summary: containerSummary
})
}
// 서버 정렬: 컨테이너 많은 순 → 이름 순
serverStatuses.sort((a, b) => {
const containerDiff = (b.container_summary?.total || 0) - (a.container_summary?.total || 0)
if (containerDiff !== 0) return containerDiff
return a.server_name.localeCompare(b.server_name)
})
return {
summary: { servers: summaryServers, containers: summaryContainers },
servers: serverStatuses,
timestamp: new Date().toISOString()
}
}
function startAutoRefresh(client: Client) {
if (client.timer) {
clearInterval(client.timer)
}
if (client.autoRefresh) {
client.timer = setInterval(() => {
const data = getNetworkStatus()
client.ws.send(JSON.stringify({ type: 'status', data }))
// 서버 대시보드 데이터도 전송
const serverData = getServerDashboard()
client.ws.send(JSON.stringify({ type: 'server', data: serverData }))
}, client.interval)
}
}
export default defineWebSocketHandler({
open(peer) {
console.log('[WebSocket] Client connected')
const client: Client = {
ws: peer,
interval: 60 * 1000, // 기본 1분
timer: null,
autoRefresh: true
}
clients.set(peer, client)
// 초기 데이터 전송
const data = getNetworkStatus()
peer.send(JSON.stringify({ type: 'status', data }))
// 서버 대시보드 데이터 전송
const serverData = getServerDashboard()
peer.send(JSON.stringify({ type: 'server', data: serverData }))
// 자동 갱신 시작
startAutoRefresh(client)
},
message(peer, message) {
const client = clients.get(peer)
if (!client) return
try {
const msg = JSON.parse(message.text())
switch (msg.type) {
case 'set_interval':
// 간격 변경 (분 단위로 받음)
client.interval = msg.interval * 60 * 1000
console.log(`[WebSocket] Interval changed to ${msg.interval} min`)
startAutoRefresh(client)
break
case 'set_auto_refresh':
// 자동 갱신 ON/OFF
client.autoRefresh = msg.enabled
console.log(`[WebSocket] Auto refresh: ${msg.enabled}`)
if (msg.enabled) {
startAutoRefresh(client)
} else if (client.timer) {
clearInterval(client.timer)
client.timer = null
}
break
case 'fetch_at':
// 특정 시간 데이터 조회
const historicalData = getHistoricalData(msg.datetime)
peer.send(JSON.stringify({ type: 'historical', data: historicalData }))
break
case 'refresh':
// 즉시 갱신 요청
const currentData = getNetworkStatus()
peer.send(JSON.stringify({ type: 'status', data: currentData }))
// 서버 데이터도 전송
const currentServerData = getServerDashboard()
peer.send(JSON.stringify({ type: 'server', data: currentServerData }))
break
case 'refresh_server':
// 서버 대시보드만 즉시 갱신
const serverDashData = getServerDashboard()
peer.send(JSON.stringify({ type: 'server', data: serverDashData }))
break
}
} catch (err) {
console.error('[WebSocket] Message parse error:', err)
}
},
close(peer) {
console.log('[WebSocket] Client disconnected')
const client = clients.get(peer)
if (client?.timer) {
clearInterval(client.timer)
}
clients.delete(peer)
},
error(peer, error) {
console.error('[WebSocket] Error:', error)
}
})