feat(monitor): enhance monitor dashboard with real-time physical host metrics
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user