335 lines
9.5 KiB
TypeScript
335 lines
9.5 KiB
TypeScript
import { query, queryOne } from '../utils/db'
|
|
|
|
interface Client {
|
|
ws: any
|
|
interval: number
|
|
timer: ReturnType<typeof setInterval> | null
|
|
autoRefresh: boolean
|
|
}
|
|
|
|
const clients = new Map<any, Client>()
|
|
|
|
async function getNetworkStatus() {
|
|
// pubnet 상태
|
|
const pubnetStatus = await queryOne(`
|
|
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
|
|
`)
|
|
|
|
const pubnetLogs = await query(`
|
|
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
|
|
`)
|
|
|
|
// privnet 상태
|
|
const privnetStatus = await queryOne(`
|
|
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
|
|
`)
|
|
|
|
const privnetLogs = await query(`
|
|
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
|
|
`)
|
|
|
|
return {
|
|
pubnet: {
|
|
status: pubnetStatus,
|
|
logs: pubnetLogs
|
|
},
|
|
privnet: {
|
|
status: privnetStatus,
|
|
logs: privnetLogs
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
|
|
async function getHistoricalData(datetime: string) {
|
|
const pubnetLogs = await query(`
|
|
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 <= $1
|
|
ORDER BY pl.checked_at DESC
|
|
LIMIT 5
|
|
`, [datetime])
|
|
|
|
const privnetLogs = await query(`
|
|
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 <= $1
|
|
ORDER BY pl.checked_at DESC
|
|
LIMIT 5
|
|
`, [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
|
|
}
|
|
}
|
|
|
|
// 임계값 조회
|
|
async function getThresholds() {
|
|
let rows: any[] = []
|
|
try {
|
|
rows = await query(`SELECT category, metric, warning, critical, danger FROM thresholds`)
|
|
} catch (e) {
|
|
// 테이블 없으면 빈 배열
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 서버 대시보드 데이터
|
|
async function getServerDashboard() {
|
|
const thresholds = await getThresholds()
|
|
const now = new Date()
|
|
const offlineThreshold = 5 * 60 * 1000 // 5분
|
|
|
|
// 서버 목록
|
|
const servers = await query(`
|
|
SELECT target_id, server_name, is_active
|
|
FROM server_targets
|
|
WHERE is_active = 1
|
|
ORDER BY server_name
|
|
`)
|
|
|
|
const serverStatuses: any[] = []
|
|
const summaryServers = { total: servers.length, normal: 0, warning: 0, critical: 0, danger: 0, offline: 0 }
|
|
|
|
for (const server of servers) {
|
|
// 최신 로그
|
|
const snapshot = await queryOne(`
|
|
SELECT cpu_usage as cpu_percent, memory_usage as memory_percent, disk_usage as disk_percent, checked_at as collected_at
|
|
FROM server_logs
|
|
WHERE target_id = $1 AND is_success = 1
|
|
ORDER BY checked_at DESC
|
|
LIMIT 1
|
|
`, [server.target_id])
|
|
|
|
// 오프라인 체크
|
|
let isOffline = true
|
|
let lastCollected = null
|
|
if (snapshot && snapshot.collected_at) {
|
|
lastCollected = snapshot.collected_at
|
|
const collectedTime = new Date(snapshot.collected_at).getTime()
|
|
isOffline = (now.getTime() - collectedTime) > offlineThreshold
|
|
}
|
|
|
|
// 서버 레벨 계산
|
|
let serverLevel = 'offline'
|
|
let cpuLevel = 'normal', memLevel = 'normal', diskLevel = 'normal'
|
|
|
|
if (!isOffline && snapshot) {
|
|
cpuLevel = getLevel(Number(snapshot.cpu_percent), thresholds.server?.cpu || { warning: 70, critical: 85, danger: 95 })
|
|
memLevel = getLevel(Number(snapshot.memory_percent), thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 })
|
|
diskLevel = getLevel(Number(snapshot.disk_percent), thresholds.server?.disk || { warning: 80, critical: 90, danger: 95 })
|
|
serverLevel = getHighestLevel([cpuLevel, memLevel, diskLevel])
|
|
}
|
|
|
|
// 요약 집계
|
|
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: snapshot?.disk_percent ?? null,
|
|
disk_level: diskLevel,
|
|
last_collected: lastCollected
|
|
})
|
|
}
|
|
|
|
// 서버 정렬: 이름 순
|
|
serverStatuses.sort((a, b) => a.server_name.localeCompare(b.server_name))
|
|
|
|
return {
|
|
summary: { servers: summaryServers },
|
|
servers: serverStatuses,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
|
|
function startAutoRefresh(client: Client) {
|
|
if (client.timer) {
|
|
clearInterval(client.timer)
|
|
}
|
|
|
|
if (client.autoRefresh) {
|
|
client.timer = setInterval(async () => {
|
|
const data = await getNetworkStatus()
|
|
client.ws.send(JSON.stringify({ type: 'status', data }))
|
|
|
|
const serverData = await getServerDashboard()
|
|
client.ws.send(JSON.stringify({ type: 'server', data: serverData }))
|
|
}, client.interval)
|
|
}
|
|
}
|
|
|
|
export default defineWebSocketHandler({
|
|
async open(peer) {
|
|
console.log('[WebSocket] Client connected')
|
|
|
|
const client: Client = {
|
|
ws: peer,
|
|
interval: 60 * 1000,
|
|
timer: null,
|
|
autoRefresh: true
|
|
}
|
|
|
|
clients.set(peer, client)
|
|
|
|
// 초기 데이터 전송
|
|
const data = await getNetworkStatus()
|
|
peer.send(JSON.stringify({ type: 'status', data }))
|
|
|
|
const serverData = await getServerDashboard()
|
|
peer.send(JSON.stringify({ type: 'server', data: serverData }))
|
|
|
|
startAutoRefresh(client)
|
|
},
|
|
|
|
async 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':
|
|
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 = await getHistoricalData(msg.datetime)
|
|
peer.send(JSON.stringify({ type: 'historical', data: historicalData }))
|
|
break
|
|
|
|
case 'refresh':
|
|
const currentData = await getNetworkStatus()
|
|
peer.send(JSON.stringify({ type: 'status', data: currentData }))
|
|
const currentServerData = await getServerDashboard()
|
|
peer.send(JSON.stringify({ type: 'server', data: currentServerData }))
|
|
break
|
|
|
|
case 'refresh_server':
|
|
const serverDashData = await 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)
|
|
}
|
|
})
|