diff --git a/backend/routes/_ws.ts b/backend/routes/_ws.ts index 4ef6442..7a6e85c 100644 --- a/backend/routes/_ws.ts +++ b/backend/routes/_ws.ts @@ -210,17 +210,10 @@ async function getServerDashboard() { let serverLevel = 'offline' let cpuLevel = 'normal', memLevel = 'normal', diskLevel = 'normal' - // 메모리 퍼센트 계산: (total - free) / total * 100 - let calculatedMemPercent = 0 - if (snapshot) { - const memTotal = Number(snapshot.memory_total) || 0 - const memFree = Number(snapshot.memory_free) || 0 - calculatedMemPercent = memTotal > 0 ? ((memTotal - memFree) / memTotal) * 100 : 0 - } - if (!isOffline && snapshot) { cpuLevel = getLevel(Number(snapshot.cpu_percent), thresholds.server?.cpu || { warning: 70, critical: 85, danger: 95 }) - memLevel = getLevel(calculatedMemPercent, thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 }) + // 메모리: snapshot.memory_percent 직접 사용 + memLevel = getLevel(Number(snapshot.memory_percent), thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 }) diskLevel = getLevel(Number(diskData?.disk_percent), thresholds.server?.disk || { warning: 80, critical: 90, danger: 95 }) serverLevel = getHighestLevel([cpuLevel, memLevel, diskLevel]) } @@ -300,7 +293,7 @@ async function getServerDashboard() { level: serverLevel, cpu_percent: snapshot?.cpu_percent ?? null, cpu_level: cpuLevel, - memory_percent: calculatedMemPercent, + memory_percent: snapshot?.memory_percent ?? null, // snapshot의 memory_percent 직접 사용 memory_level: memLevel, memory_total: snapshot?.memory_total ?? null, memory_free: snapshot?.memory_free ?? null, diff --git a/backend/utils/server-scheduler.ts b/backend/utils/server-scheduler.ts index 58b3724..dacbaee 100644 --- a/backend/utils/server-scheduler.ts +++ b/backend/utils/server-scheduler.ts @@ -70,7 +70,7 @@ async function detectAnomalies(targetId: number, serverName: string) { const SHORT_TERM_THRESHOLD = 30 const snapshots = await query(` - SELECT cpu_percent, memory_total, memory_free + SELECT cpu_percent, memory_percent FROM server_snapshots WHERE target_id = $1 AND is_online = 1 ORDER BY collected_at DESC @@ -85,14 +85,9 @@ async function detectAnomalies(targetId: number, serverName: string) { 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 - // 메모리: (total - free) / total * 100 - const calcMemPct = (s: any) => { - const total = Number(s.memory_total) || 0 - const free = Number(s.memory_free) || 0 - return total > 0 ? ((total - free) / total) * 100 : 0 - } - const currMemAvg = currSnapshots.reduce((sum, s) => sum + calcMemPct(s), 0) / currSnapshots.length - const prevMemAvg = prevSnapshots.reduce((sum, s) => sum + calcMemPct(s), 0) / prevSnapshots.length + // 메모리: memory_percent 직접 사용 + 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 const cpuChange = prevCpuAvg > 1 ? ((currCpuAvg - prevCpuAvg) / prevCpuAvg) * 100 : currCpuAvg - prevCpuAvg const memChange = prevMemAvg > 1 ? ((currMemAvg - prevMemAvg) / prevMemAvg) * 100 : currMemAvg - prevMemAvg @@ -149,7 +144,7 @@ async function detectAnomalies(targetId: number, serverName: string) { const DANGER_Z = 3.0 const hourSnapshots = await query(` - SELECT cpu_percent, memory_total, memory_free + SELECT cpu_percent, memory_percent FROM server_snapshots WHERE target_id = $1 AND is_online = 1 AND collected_at::timestamp >= NOW() - INTERVAL '1 hour' @@ -159,16 +154,10 @@ async function detectAnomalies(targetId: number, serverName: string) { if (hourSnapshots.length >= 10) { const current = hourSnapshots[0] const currCpu = current.cpu_percent ?? 0 - // 메모리: (total - free) / total * 100 - const calcMemPctZ = (s: any) => { - const total = Number(s.memory_total) || 0 - const free = Number(s.memory_free) || 0 - return total > 0 ? ((total - free) / total) * 100 : 0 - } - const currMem = calcMemPctZ(current) + const currMem = current.memory_percent ?? 0 // memory_percent 직접 사용 const cpuValues = hourSnapshots.map(s => s.cpu_percent ?? 0) - const memValues = hourSnapshots.map(s => calcMemPctZ(s)) + const memValues = hourSnapshots.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 @@ -236,7 +225,7 @@ async function detectAnomalies(targetId: number, serverName: string) { const dayType = isWeekend ? 'weekend' : 'weekday' const baselineData = await query(` - SELECT cpu_percent, memory_total, memory_free + SELECT cpu_percent, memory_percent FROM server_snapshots WHERE target_id = $1 AND is_online = 1 AND collected_at::timestamp >= NOW() - INTERVAL '14 days' @@ -249,25 +238,18 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId, currentHour, dayType]) const currentSnapshot = await queryOne(` - SELECT cpu_percent, memory_total, memory_free + SELECT cpu_percent, memory_percent FROM server_snapshots WHERE target_id = $1 AND is_online = 1 ORDER BY collected_at DESC LIMIT 1 `, [targetId]) - // 메모리 계산 함수: (total - free) / total * 100 - const calcMemPctBaseline = (s: any) => { - const total = Number(s.memory_total) || 0 - const free = Number(s.memory_free) || 0 - return total > 0 ? ((total - free) / total) * 100 : 0 - } - if (baselineData.length >= 5 && currentSnapshot) { const currCpu = currentSnapshot.cpu_percent ?? 0 - const currMem = calcMemPctBaseline(currentSnapshot) + const currMem = currentSnapshot.memory_percent ?? 0 // memory_percent 직접 사용 const cpuValues = baselineData.map(s => s.cpu_percent ?? 0) - const memValues = baselineData.map(s => calcMemPctBaseline(s)) + const memValues = baselineData.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 @@ -506,7 +488,7 @@ async function collectServerData(target: ServerTarget) { quicklook?.cpu_number || quicklook?.cpu_log_core || cpu?.cpucore || null, cpu?.total ?? quicklook?.cpu ?? null, mem?.total || null, - mem?.used || null, + mem?.total && mem?.percent ? (mem.total * mem.percent / 100) : null, // memory_used = total × percent / 100 mem?.free || null, mem?.percent || null, memswap?.total || null, @@ -548,6 +530,26 @@ async function collectServerData(target: ServerTarget) { if (Array.isArray(docker) && docker.length > 0) { console.log(`[${now}] 🐳 [${target.server_name}] container 저장 (${docker.length}개 컨테이너)`) for (const container of docker) { + // CPU 값 추출 로직 + const cpuPercent = container.cpu?.total ?? container.cpu_percent ?? null + const memoryUsage = container.memory?.usage || container.memory_usage || null + const memoryLimit = container.memory?.limit || container.memory_limit || null + const memoryPercent = container.memory?.usage && container.memory?.limit + ? (container.memory.usage / container.memory.limit * 100) + : container.memory_percent ?? null + + // 각 컨테이너별 상세 로그 + console.log(`[${now}] 🐳 - ${container.name}: CPU=${cpuPercent?.toFixed(2) ?? 'null'}%, MEM=${memoryPercent?.toFixed(2) ?? 'null'}%`) + + // CPU가 null인 경우 상세 디버깅 + if (cpuPercent === null) { + console.warn(`[${now}] ⚠️ - ${container.name}: CPU 값 null! 원본:`, JSON.stringify({ + 'cpu.total': container.cpu?.total, + 'cpu_percent': container.cpu_percent, + 'cpu': container.cpu + })) + } + await execute(` INSERT INTO server_containers ( target_id, docker_id, container_name, container_image, @@ -560,12 +562,10 @@ async function collectServerData(target: ServerTarget) { container.name || null, Array.isArray(container.image) ? container.image.join(', ') : container.image || null, container.status || null, - container.cpu?.total ?? container.cpu_percent ?? null, - container.memory?.usage || container.memory_usage || null, - container.memory?.limit || container.memory_limit || null, - container.memory?.usage && container.memory?.limit - ? (container.memory.usage / container.memory.limit * 100) - : container.memory_percent ?? null, + cpuPercent, + memoryUsage, + memoryLimit, + memoryPercent, container.uptime || null, container.network?.rx ?? container.network_rx ?? null, container.network?.tx ?? container.network_tx ?? null, diff --git a/frontend/components/ServerPortlet.vue b/frontend/components/ServerPortlet.vue index 5f158d2..ef1c55d 100644 --- a/frontend/components/ServerPortlet.vue +++ b/frontend/components/ServerPortlet.vue @@ -297,20 +297,16 @@ function isContainerChanged(serverId: number, containerName: string, metric: str return changedKeys.value.has(`container-${serverId}-${containerName}-${metric}`) } -// 서버 메모리 퍼센트 계산: (total - free) / total * 100 +// 서버 메모리 퍼센트: memory_percent 직접 사용 function calcMemPercent(server: ServerStatus): number { - const total = Number(server.memory_total) || 0 - const free = Number(server.memory_free) || 0 - if (total === 0) return 0 - return ((total - free) / total) * 100 + return server.memory_percent || 0 } -// 서버 메모리 용량 포맷: used/total GB +// 서버 메모리 용량 포맷: used/total GB (memory_used는 DB에서 계산된 값) function formatServerMem(server: ServerStatus): string { + const used = Number(server.memory_used) || 0 const total = Number(server.memory_total) || 0 - const free = Number(server.memory_free) || 0 if (total === 0) return '-' - const used = total - free const usedGB = (used / (1024 * 1024 * 1024)).toFixed(1) const totalGB = (total / (1024 * 1024 * 1024)).toFixed(1) return `${usedGB}/${totalGB}G` diff --git a/frontend/server/history.vue b/frontend/server/history.vue index eaf3dea..2972e99 100644 --- a/frontend/server/history.vue +++ b/frontend/server/history.vue @@ -405,13 +405,8 @@ async function fetchSnapshots() { load: validLoad.length ? (validLoad.reduce((a: number, b: number) => a + b, 0) / validLoad.length).toFixed(1) : '-' } - // Memory/Swap 라인 차트 - 퍼센트 계산: (total - free) / total * 100 - const memData = data.map((d: any) => { - const total = Number(d.memory_total) || 0 - const free = Number(d.memory_free) || 0 - if (total === 0) return 0 - return ((total - free) / total) * 100 - }) + // Memory/Swap 라인 차트 - memory_percent 직접 사용 + const memData = data.map((d: any) => d.memory_percent || 0) const swapData = data.map((d: any) => { const total = Number(d.swap_total) || 0 const used = Number(d.swap_used) || 0 @@ -424,16 +419,10 @@ async function fetchSnapshots() { ]) // 평균 계산 (Memory, Swap) + 사용량/전체용량 (BigInt는 문자열로 반환되므로 Number로 변환) - // 메모리 사용량 = total - free (free가 있으면 사용, 없으면 used 사용) - // 메모리 퍼센트도 (total - free) / total * 100 으로 계산 + // 메모리 사용량 = memory_used (DB에서 계산된 값) const validMem = memData.filter((v: number) => v > 0) const validSwap = swapData.filter((v: number) => v >= 0) - const memUsedData = data.map((d: any) => { - const total = Number(d.memory_total) || 0 - const free = Number(d.memory_free) || 0 - const used = Number(d.memory_used) || 0 - return free > 0 ? (total - free) : used - }).filter((v: number) => v > 0) + const memUsedData = data.map((d: any) => Number(d.memory_used) || 0).filter((v: number) => v > 0) const avgMemUsedGB = memUsedData.length ? (memUsedData.reduce((a: number, b: number) => a + b, 0) / memUsedData.length / (1024 * 1024 * 1024)).toFixed(1) : '-' const memTotalGB = data[0]?.memory_total ? (Number(data[0].memory_total) / (1024 * 1024 * 1024)).toFixed(1) : '-' const swapUsedData = data.map((d: any) => Number(d.swap_used) || 0).filter((v: number) => v >= 0) diff --git a/package-lock.json b/package-lock.json index 9ed2523..a81d723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3294,7 +3293,6 @@ "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3543,7 +3541,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.26", @@ -3714,7 +3711,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4059,7 +4055,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4161,7 +4156,6 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4239,7 +4233,6 @@ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "license": "MIT", - "peer": true, "dependencies": { "consola": "^3.2.3" } @@ -7645,7 +7638,6 @@ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.102.0.tgz", "integrity": "sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==", "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "^0.102.0" }, @@ -7829,7 +7821,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -7962,7 +7953,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8714,7 +8704,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9575,7 +9564,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10021,7 +10009,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10378,7 +10365,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -10415,7 +10401,6 @@ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^6.6.4" },