feat(monitor): enhance monitor dashboard with real-time physical host metrics

This commit is contained in:
duorameng
2026-06-12 19:44:57 +08:00
parent 633d683c0e
commit d5c4680fc5
3 changed files with 141 additions and 7 deletions
@@ -3,6 +3,7 @@ package controllers
import ( import (
"net/http" "net/http"
"runtime" "runtime"
"sync"
"time" "time"
"github.com/engigu/baihu-panel/internal/services/tasks" "github.com/engigu/baihu-panel/internal/services/tasks"
@@ -10,10 +11,22 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
) )
type MonitorController struct { type MonitorController struct {
executorService *tasks.ExecutorService executorService *tasks.ExecutorService
// 缓存物理机状态
hostMu sync.RWMutex
lastUpdate time.Time
cpuPercent float64
vMem *mem.VirtualMemoryStat
diskUsage *disk.UsageStat
hostInfo *host.InfoStat
} }
func NewMonitorController(executorService *tasks.ExecutorService) *MonitorController { func NewMonitorController(executorService *tasks.ExecutorService) *MonitorController {
@@ -69,10 +82,50 @@ func (mc *MonitorController) sendMonitorData(ws *websocket.Conn) error {
}) })
} }
func (mc *MonitorController) updateHostMetrics() {
mc.hostMu.Lock()
defer mc.hostMu.Unlock()
// 缓存 2 秒
if time.Since(mc.lastUpdate) < 2*time.Second && mc.vMem != nil {
return
}
cpuPercents, _ := cpu.Percent(0, false)
if len(cpuPercents) > 0 {
mc.cpuPercent = cpuPercents[0]
}
mc.vMem, _ = mem.VirtualMemory()
mc.diskUsage, _ = disk.Usage("/")
mc.hostInfo, _ = host.Info()
mc.lastUpdate = time.Now()
}
func (mc *MonitorController) getMonitorData() gin.H { func (mc *MonitorController) getMonitorData() gin.H {
var m runtime.MemStats var m runtime.MemStats
runtime.ReadMemStats(&m) runtime.ReadMemStats(&m)
// 更新并读取缓存的物理主机指标
mc.updateHostMetrics()
mc.hostMu.RLock()
cpuPercent := mc.cpuPercent
vMem := mc.vMem
diskUsage := mc.diskUsage
hostInfo := mc.hostInfo
mc.hostMu.RUnlock()
// 提供默认值防空指针
if vMem == nil {
vMem = &mem.VirtualMemoryStat{}
}
if diskUsage == nil {
diskUsage = &disk.UsageStat{}
}
if hostInfo == nil {
hostInfo = &host.InfoStat{}
}
return gin.H{ return gin.H{
"env": gin.H{ "env": gin.H{
"os": runtime.GOOS, "os": runtime.GOOS,
@@ -81,6 +134,17 @@ func (mc *MonitorController) getMonitorData() gin.H {
"num_cpu": runtime.NumCPU(), "num_cpu": runtime.NumCPU(),
"goroutines": runtime.NumGoroutine(), "goroutines": runtime.NumGoroutine(),
}, },
"host": gin.H{
"cpu_percent": cpuPercent,
"mem_total": vMem.Total,
"mem_used": vMem.Used,
"mem_percent": vMem.UsedPercent,
"disk_total": diskUsage.Total,
"disk_used": diskUsage.Used,
"disk_percent": diskUsage.UsedPercent,
"uptime": hostInfo.Uptime,
"platform": hostInfo.Platform + " " + hostInfo.PlatformVersion,
},
"mem": gin.H{ "mem": gin.H{
"alloc": m.Alloc, "alloc": m.Alloc,
"total_alloc": m.TotalAlloc, "total_alloc": m.TotalAlloc,
+11
View File
@@ -17,6 +17,17 @@ export interface MonitorStats {
num_cpu: number num_cpu: number
goroutines: number goroutines: number
} }
host: {
cpu_percent: number
mem_total: number
mem_used: number
mem_percent: number
disk_total: number
disk_used: number
disk_percent: number
uptime: number
platform: string
}
mem: { mem: {
alloc: number alloc: number
total_alloc: number total_alloc: number
+66 -7
View File
@@ -4,7 +4,8 @@ import type { MonitorStats } from '@/api'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { RefreshCw } from 'lucide-vue-next' import StatusDot from '@/components/StatusDot.vue'
import { RefreshCw, Zap, LayoutList, FileText, Server, Cpu, MemoryStick, HardDrive } from 'lucide-vue-next'
import { import {
Chart as ChartJS, Chart as ChartJS,
@@ -298,7 +299,61 @@ const schedulerChartData = computed(() => ({
</div> </div>
<TabsContent value="dashboard" class="mt-0 space-y-4"> <TabsContent value="dashboard" class="mt-0 space-y-4">
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 物理主机监控环形图 -->
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base font-medium text-muted-foreground flex items-center"><Cpu class="w-4 h-4 mr-2" /> CPU 使用率</CardTitle>
</CardHeader>
<CardContent class="flex flex-col items-center">
<div class="relative w-32 h-32 flex items-center justify-center mt-2">
<svg class="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path class="text-muted/20" stroke-width="3" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path :class="stats.host.cpu_percent > 80 ? 'text-red-500' : 'text-blue-500'" stroke-dasharray="100, 100" :stroke-dashoffset="100 - stats.host.cpu_percent" stroke-width="3" stroke-linecap="round" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" style="transition: stroke-dashoffset 0.5s ease 0s;" />
</svg>
<div class="absolute text-2xl font-bold" :class="stats.host.cpu_percent > 80 ? 'text-red-500' : 'text-foreground'">{{ stats.host.cpu_percent.toFixed(1) }}%</div>
</div>
<div class="text-xs text-muted-foreground mt-4">宿主机物理核心负载</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base font-medium text-muted-foreground flex items-center"><MemoryStick class="w-4 h-4 mr-2" /> 内存使用率</CardTitle>
</CardHeader>
<CardContent class="flex flex-col items-center">
<div class="relative w-32 h-32 flex items-center justify-center mt-2">
<svg class="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path class="text-muted/20" stroke-width="3" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path :class="stats.host.mem_percent > 80 ? 'text-red-500' : 'text-emerald-500'" stroke-dasharray="100, 100" :stroke-dashoffset="100 - stats.host.mem_percent" stroke-width="3" stroke-linecap="round" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" style="transition: stroke-dashoffset 0.5s ease 0s;" />
</svg>
<div class="absolute text-2xl font-bold" :class="stats.host.mem_percent > 80 ? 'text-red-500' : 'text-foreground'">{{ stats.host.mem_percent.toFixed(1) }}%</div>
</div>
<div class="text-xs text-muted-foreground mt-4">{{ formatBytes(stats.host.mem_used) }} / {{ formatBytes(stats.host.mem_total) }}</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base font-medium text-muted-foreground flex items-center"><HardDrive class="w-4 h-4 mr-2" /> 磁盘使用率</CardTitle>
</CardHeader>
<CardContent class="flex flex-col items-center">
<div class="relative w-32 h-32 flex items-center justify-center mt-2">
<svg class="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path class="text-muted/20" stroke-width="3" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path :class="stats.host.disk_percent > 80 ? 'text-red-500' : 'text-purple-500'" stroke-dasharray="100, 100" :stroke-dashoffset="100 - stats.host.disk_percent" stroke-width="3" stroke-linecap="round" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" style="transition: stroke-dashoffset 0.5s ease 0s;" />
</svg>
<div class="absolute text-2xl font-bold" :class="stats.host.disk_percent > 80 ? 'text-red-500' : 'text-foreground'">{{ stats.host.disk_percent.toFixed(1) }}%</div>
</div>
<div class="text-xs text-muted-foreground mt-4">{{ formatBytes(stats.host.disk_used) }} / {{ formatBytes(stats.host.disk_total) }}</div>
</CardContent>
</Card>
</div>
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<Card> <Card>
<CardHeader class="pb-2"> <CardHeader class="pb-2">
<CardTitle class="text-base text-blue-600">执行环境</CardTitle> <CardTitle class="text-base text-blue-600">执行环境</CardTitle>
@@ -339,10 +394,12 @@ const schedulerChartData = computed(() => ({
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="font-semibold text-sm">Worker #{{ worker.id }}</span> <span class="font-semibold text-sm">Worker #{{ worker.id }}</span>
<span v-if="worker.status === 'idle'" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300"> <span v-if="worker.status === 'idle'" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300">
🟢 空闲 (Idle) <StatusDot state="online" class="mr-1.5" />
空闲 (Idle)
</span> </span>
<span v-else class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300"> <span v-else class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">
执行中 (Running) <StatusDot state="running" class="mr-1.5" />
执行中 (Running)
</span> </span>
</div> </div>
<div v-if="worker.status === 'running'" class="text-xs text-muted-foreground mt-1 space-y-1"> <div v-if="worker.status === 'running'" class="text-xs text-muted-foreground mt-1 space-y-1">
@@ -356,7 +413,9 @@ const schedulerChartData = computed(() => ({
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div>
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
<Card> <Card>
<CardHeader class="pb-2"> <CardHeader class="pb-2">
<CardTitle class="text-base text-emerald-600">内存概览</CardTitle> <CardTitle class="text-base text-emerald-600">内存概览</CardTitle>
@@ -395,9 +454,9 @@ const schedulerChartData = computed(() => ({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<dl class="space-y-1 text-sm"> <dl class="space-y-1 text-sm">
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">下次 GC 目标 (NextGC)</dt><dd class="font-medium">{{ formatBytes(stats.gc.next_gc) }}</dd></div> <div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">下次 GC目标 (NextGC)</dt><dd class="font-medium">{{ formatBytes(stats.gc.next_gc) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">上次 GC 时间 (LastGC)</dt><dd class="font-medium">{{ stats.gc.last_gc ? new Date(stats.gc.last_gc / 1000000).toLocaleString() : '-' }}</dd></div> <div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">上次 GC时间 (LastGC)</dt><dd class="font-medium">{{ stats.gc.last_gc ? new Date(stats.gc.last_gc / 1000000).toLocaleString() : '-' }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">GC 总停顿时间 (PauseTotalNs)</dt><dd class="font-medium">{{ formatNs(stats.gc.pause_total_ns) }}</dd></div> <div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">GC停顿总时 (PauseTotal)</dt><dd class="font-medium">{{ formatNs(stats.gc.pause_total_ns) }}</dd></div>
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">执行次数 (NumGC)</dt><dd class="font-medium">{{ stats.gc.num_gc }}</dd></div> <div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">执行次数 (NumGC)</dt><dd class="font-medium">{{ stats.gc.num_gc }}</dd></div>
</dl> </dl>
</CardContent> </CardContent>