feat: implement precise worker status tracking and monitoring UI
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/services/tasks"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type MonitorController struct {
|
||||
executorService *tasks.ExecutorService
|
||||
}
|
||||
|
||||
func NewMonitorController(executorService *tasks.ExecutorService) *MonitorController {
|
||||
return &MonitorController{
|
||||
executorService: executorService,
|
||||
}
|
||||
}
|
||||
|
||||
var monitorUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 开发环境允许所有跨域,生产环境可根据配置限制
|
||||
},
|
||||
}
|
||||
|
||||
// GetSystemMonitor 获取系统和内存监控信息 (HTTP)
|
||||
func (mc *MonitorController) GetSystemMonitor(c *gin.Context) {
|
||||
data := mc.getMonitorData()
|
||||
utils.Success(c, data)
|
||||
}
|
||||
|
||||
// MonitorWS WebSocket实时推送系统监控信息
|
||||
func (mc *MonitorController) MonitorWS(c *gin.Context) {
|
||||
ws, err := monitorUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 先立即发送一次
|
||||
mc.sendMonitorData(ws)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := mc.sendMonitorData(ws); err != nil {
|
||||
return // 客户端断开连接或发送失败
|
||||
}
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MonitorController) sendMonitorData(ws *websocket.Conn) error {
|
||||
data := mc.getMonitorData()
|
||||
return ws.WriteJSON(gin.H{
|
||||
"code": 200,
|
||||
"data": data,
|
||||
"msg": "success",
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MonitorController) getMonitorData() gin.H {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
return gin.H{
|
||||
"env": gin.H{
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"go_version": runtime.Version(),
|
||||
"num_cpu": runtime.NumCPU(),
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
},
|
||||
"mem": gin.H{
|
||||
"alloc": m.Alloc,
|
||||
"total_alloc": m.TotalAlloc,
|
||||
"sys": m.Sys,
|
||||
"lookups": m.Lookups,
|
||||
"mallocs": m.Mallocs,
|
||||
"frees": m.Frees,
|
||||
},
|
||||
"heap": gin.H{
|
||||
"heap_alloc": m.HeapAlloc,
|
||||
"heap_sys": m.HeapSys,
|
||||
"heap_idle": m.HeapIdle,
|
||||
"heap_inuse": m.HeapInuse,
|
||||
"heap_released": m.HeapReleased,
|
||||
"heap_objects": m.HeapObjects,
|
||||
},
|
||||
"gc": gin.H{
|
||||
"next_gc": m.NextGC,
|
||||
"last_gc": m.LastGC,
|
||||
"pause_total_ns": m.PauseTotalNs,
|
||||
"num_gc": m.NumGC,
|
||||
},
|
||||
"scheduler": gin.H{
|
||||
"scheduled": mc.executorService.GetScheduledCount(),
|
||||
"running": mc.executorService.GetRunningCount(),
|
||||
"queue_size": mc.executorService.GetScheduler().GetQueueSize(),
|
||||
"worker_count": mc.executorService.GetScheduler().GetConfig().WorkerCount,
|
||||
"workers": mc.executorService.GetScheduler().GetWorkerStatuses(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,16 @@ func (h *schedulerHooksAdapter) OnHeartbeat(ctx context.Context, logID string, d
|
||||
// TaskExecutor 定义任务执行函数签名
|
||||
type TaskExecutor func(ctx context.Context, req *ExecutionRequest, stdout, stderr io.Writer) (*Result, error)
|
||||
|
||||
// WorkerStatus 定义并发池中单个 Worker 的状态
|
||||
type WorkerStatus struct {
|
||||
ID int `json:"id"`
|
||||
Status string `json:"status"` // 状态: "idle" 或 "running"
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
TaskName string `json:"task_name,omitempty"`
|
||||
StartTime int64 `json:"start_time,omitempty"` // 开始时间戳 (秒)
|
||||
Duration int64 `json:"duration,omitempty"` // 已运行时长 (秒)
|
||||
}
|
||||
|
||||
// Scheduler 统一调度器(独立组件,可在主服务和 Agent 中复用)
|
||||
// 调度器本身只负责队列管理和任务调度,具体的执行逻辑和事件处理由 Handler 实现
|
||||
type Scheduler struct {
|
||||
@@ -188,6 +198,9 @@ type Scheduler struct {
|
||||
logger SchedulerLogger
|
||||
runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 (TaskID -> CancelFunc)
|
||||
runningExecs map[string]context.CancelFunc // 记录运行中的执行,用于停止 (LogID -> CancelFunc)
|
||||
|
||||
workers []WorkerStatus
|
||||
workerMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewScheduler 创建调度器
|
||||
@@ -224,6 +237,14 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu
|
||||
logger: &DefaultLogger{},
|
||||
runningTasks: make(map[string]context.CancelFunc),
|
||||
runningExecs: make(map[string]context.CancelFunc),
|
||||
workers: make([]WorkerStatus, config.WorkerCount),
|
||||
}
|
||||
|
||||
for i := 0; i < config.WorkerCount; i++ {
|
||||
s.workers[i] = WorkerStatus{
|
||||
ID: i,
|
||||
Status: "idle",
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -332,7 +353,32 @@ func (s *Scheduler) worker(id int) {
|
||||
}()
|
||||
// 速率限制
|
||||
<-s.rateLimiter
|
||||
s.executeTask(req)
|
||||
|
||||
func() {
|
||||
// 恢复 worker 状态为空闲
|
||||
defer func() {
|
||||
s.workerMu.Lock()
|
||||
if id >= 0 && id < len(s.workers) {
|
||||
s.workers[id].Status = "idle"
|
||||
s.workers[id].TaskID = ""
|
||||
s.workers[id].TaskName = ""
|
||||
s.workers[id].StartTime = 0
|
||||
}
|
||||
s.workerMu.Unlock()
|
||||
}()
|
||||
|
||||
// 更新 worker 状态为运行中
|
||||
s.workerMu.Lock()
|
||||
if id >= 0 && id < len(s.workers) {
|
||||
s.workers[id].Status = "running"
|
||||
s.workers[id].TaskID = req.TaskID
|
||||
s.workers[id].TaskName = req.Name
|
||||
s.workers[id].StartTime = time.Now().Unix()
|
||||
}
|
||||
s.workerMu.Unlock()
|
||||
|
||||
s.executeTask(req)
|
||||
}()
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -615,3 +661,24 @@ func (s *Scheduler) GetConfig() SchedulerConfig {
|
||||
defer s.mu.RUnlock()
|
||||
return s.config
|
||||
}
|
||||
|
||||
// GetWorkerStatuses 获取所有 Worker 的状态
|
||||
func (s *Scheduler) GetWorkerStatuses() []WorkerStatus {
|
||||
s.workerMu.RLock()
|
||||
defer s.workerMu.RUnlock()
|
||||
// 返回副本防止外部修改
|
||||
statuses := make([]WorkerStatus, len(s.workers))
|
||||
now := time.Now().Unix()
|
||||
for i, w := range s.workers {
|
||||
statuses[i] = w
|
||||
// 在服务端计算运行时间,彻底避免客户端与服务端时钟不一致导致的计算偏差
|
||||
if w.Status == "running" && w.StartTime > 0 {
|
||||
duration := now - w.StartTime
|
||||
if duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
statuses[i].Duration = duration
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) {
|
||||
registerAppLogRoutes(adminOnly, c)
|
||||
registerSystemWSRoutes(adminOnly, c)
|
||||
registerWebUIRoutes(adminOnly, c)
|
||||
registerMonitorRoutes(adminOnly, c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +269,14 @@ func registerSystemWSRoutes(g *gin.RouterGroup, c *Controllers) {
|
||||
g.GET("/ws/events", c.SystemWS.HandleEvents)
|
||||
}
|
||||
|
||||
func registerMonitorRoutes(g *gin.RouterGroup, c *Controllers) {
|
||||
monitor := g.Group("/monitor")
|
||||
{
|
||||
monitor.GET("", c.Monitor.GetSystemMonitor)
|
||||
monitor.GET("/ws", c.Monitor.MonitorWS)
|
||||
}
|
||||
}
|
||||
|
||||
func initAgentAPIRoutes(root *gin.RouterGroup, c *Controllers) {
|
||||
// Agent API(供远程 Agent 调用,不使用 /v1 版本号)
|
||||
agentAPI := root.Group("/api/agent")
|
||||
|
||||
@@ -64,6 +64,7 @@ func RegisterControllers() *Controllers {
|
||||
AppLog: controllers.NewAppLogController(),
|
||||
SystemWS: controllers.NewSystemWSController(),
|
||||
WebUI: controllers.NewWebUIController(services.NewWebUIService(settingsService)),
|
||||
Monitor: controllers.NewMonitorController(executorService),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ type Controllers struct {
|
||||
AppLog *controllers.AppLogController
|
||||
SystemWS *controllers.SystemWSController
|
||||
WebUI *controllers.WebUIController
|
||||
Monitor *controllers.MonitorController
|
||||
}
|
||||
|
||||
func Setup(c *Controllers) *gin.Engine {
|
||||
|
||||
@@ -9,6 +9,52 @@ interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface MonitorStats {
|
||||
env: {
|
||||
os: string
|
||||
arch: string
|
||||
go_version: string
|
||||
num_cpu: number
|
||||
goroutines: number
|
||||
}
|
||||
mem: {
|
||||
alloc: number
|
||||
total_alloc: number
|
||||
sys: number
|
||||
lookups: number
|
||||
mallocs: number
|
||||
frees: number
|
||||
}
|
||||
heap: {
|
||||
heap_alloc: number
|
||||
heap_sys: number
|
||||
heap_idle: number
|
||||
heap_inuse: number
|
||||
heap_released: number
|
||||
heap_objects: number
|
||||
}
|
||||
gc: {
|
||||
next_gc: number
|
||||
last_gc: number
|
||||
pause_total_ns: number
|
||||
num_gc: number
|
||||
}
|
||||
scheduler: {
|
||||
scheduled: number
|
||||
running: number
|
||||
queue_size: number
|
||||
worker_count: number
|
||||
workers: {
|
||||
id: number
|
||||
status: string
|
||||
task_id?: string
|
||||
task_name?: string
|
||||
start_time?: number
|
||||
duration?: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
@@ -145,6 +191,7 @@ export const api = {
|
||||
taskStats: (days?: number) => request<TaskStatsItem[]>(`/taskstats${days ? `?days=${days}` : ''}`)
|
||||
},
|
||||
settings: {
|
||||
getMonitor: () => request<MonitorStats>('/monitor'),
|
||||
changePassword: (data: { old_username?: string; username?: string; old_password: string; new_password?: string }) =>
|
||||
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
|
||||
getSite: () => request<SiteSettings>('/settings/site'),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||
import { resetAuthCache } from '@/router'
|
||||
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe, Bell } from 'lucide-vue-next'
|
||||
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe, Bell, Activity } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import SystemNotice from '@/components/SystemNotice.vue'
|
||||
@@ -63,6 +63,7 @@ const navItems = [
|
||||
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
||||
{ to: '/notify', icon: Bell, label: '消息推送', exact: true },
|
||||
{ to: '/logs', icon: KeyRound, label: '运行日志', exact: true },
|
||||
{ to: '/monitor', icon: Activity, label: '系统监控', exact: true },
|
||||
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
||||
]
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ const router = createRouter({
|
||||
{ path: 'logs', name: 'logs', component: () => import('@/views/logs/MessageLogs.vue') },
|
||||
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
||||
{ path: 'notify', name: 'notify', component: () => import('@/views/notify/Notify.vue') },
|
||||
{ path: 'monitor', name: 'monitor', component: () => import('@/views/monitor/Monitor.vue') },
|
||||
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import type { MonitorStats } from '@/api'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
import { Line, Bar } from 'vue-chartjs'
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
|
||||
const activeTab = ref('charts')
|
||||
const stats = ref<MonitorStats | null>(null)
|
||||
const loading = ref(false)
|
||||
let timer: any = null
|
||||
|
||||
// --- 时序数据池 ---
|
||||
const historySize = 60 // 保存最近60次请求(约3分钟@3s)
|
||||
const timeLabels = ref<string[]>([])
|
||||
const goroutinesData = ref<number[]>([])
|
||||
const allocData = ref<number[]>([])
|
||||
const sysData = ref<number[]>([])
|
||||
const gcPausesData = ref<number[]>([])
|
||||
const scheduledData = ref<number[]>([])
|
||||
const runningData = ref<number[]>([])
|
||||
const queueData = ref<number[]>([])
|
||||
let lastPauseNs = 0
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const baseUrl = (window as any).__BASE_URL__ || ''
|
||||
const apiVersion = (window as any).__API_VERSION__ || '/api/v1'
|
||||
|
||||
const connectWS = () => {
|
||||
if (timer) return
|
||||
loading.value = true
|
||||
const host = window.location.host
|
||||
const wsUrl = `${protocol}//${host}${baseUrl}${apiVersion}/monitor/ws`
|
||||
|
||||
timer = new WebSocket(wsUrl)
|
||||
|
||||
timer.onopen = () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
timer.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
if (payload.code === 200 && payload.data) {
|
||||
const res = payload.data
|
||||
stats.value = res
|
||||
|
||||
const nowStr = new Date().toLocaleTimeString('en-US', { hour12: false })
|
||||
if (timeLabels.value.length >= historySize) {
|
||||
timeLabels.value.shift()
|
||||
goroutinesData.value.shift()
|
||||
allocData.value.shift()
|
||||
sysData.value.shift()
|
||||
gcPausesData.value.shift()
|
||||
scheduledData.value.shift()
|
||||
runningData.value.shift()
|
||||
queueData.value.shift()
|
||||
}
|
||||
|
||||
timeLabels.value.push(nowStr)
|
||||
goroutinesData.value.push(res.env.goroutines)
|
||||
allocData.value.push(Number((res.mem.alloc / 1024 / 1024).toFixed(2)))
|
||||
sysData.value.push(Number((res.mem.sys / 1024 / 1024).toFixed(2)))
|
||||
|
||||
let pauseDelta = 0
|
||||
if (lastPauseNs > 0 && res.gc.pause_total_ns >= lastPauseNs) {
|
||||
pauseDelta = Number(((res.gc.pause_total_ns - lastPauseNs) / 1000000).toFixed(2))
|
||||
}
|
||||
lastPauseNs = res.gc.pause_total_ns
|
||||
gcPausesData.value.push(pauseDelta)
|
||||
scheduledData.value.push(res.scheduler.scheduled)
|
||||
runningData.value.push(res.scheduler.running)
|
||||
queueData.value.push(res.scheduler.queue_size)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse WS message error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
timer.onclose = () => {
|
||||
loading.value = false
|
||||
timer = null
|
||||
// 断线后2秒自动重连
|
||||
setTimeout(connectWS, 2000)
|
||||
}
|
||||
|
||||
timer.onerror = () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWS = () => {
|
||||
if (timer) {
|
||||
// 置空 onclose 避免触发自动重连
|
||||
timer.onclose = null
|
||||
timer.close()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const formatNs = (ns: number) => {
|
||||
if (ns < 1000) return ns + ' ns'
|
||||
if (ns < 1000000) return (ns / 1000).toFixed(2) + ' μs'
|
||||
return (ns / 1000000).toFixed(2) + ' ms'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connectWS()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectWS()
|
||||
})
|
||||
|
||||
// --- 图表配置 ---
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // 关闭过渡动画以支持实时流畅更新
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index' as const,
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
usePointStyle: false,
|
||||
boxWidth: 12,
|
||||
boxHeight: 12,
|
||||
useBorderRadius: true,
|
||||
borderRadius: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: { dash: [4, 4] },
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const goroutineChartData = computed(() => ({
|
||||
labels: [...timeLabels.value],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Goroutines (协程并发数)',
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
||||
borderColor: '#2563eb', // blue-600
|
||||
borderWidth: 2,
|
||||
data: [...goroutinesData.value],
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const memChartData = computed(() => ({
|
||||
labels: [...timeLabels.value],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Alloc (MB) 当前分配',
|
||||
backgroundColor: 'rgba(5, 150, 105, 0.1)',
|
||||
borderColor: '#059669', // emerald-600
|
||||
borderWidth: 2,
|
||||
data: [...allocData.value],
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
},
|
||||
{
|
||||
label: 'Sys (MB) 系统申请上限',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#d97706', // amber-600
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 5],
|
||||
data: [...sysData.value],
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const gcChartData = computed(() => ({
|
||||
labels: [...timeLabels.value],
|
||||
datasets: [
|
||||
{
|
||||
label: 'GC Pause (ms) 垃圾回收停顿时间',
|
||||
backgroundColor: '#ea580c', // orange-600
|
||||
data: [...gcPausesData.value],
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const schedulerChartData = computed(() => ({
|
||||
labels: [...timeLabels.value],
|
||||
datasets: [
|
||||
{
|
||||
label: '正在运行 (Running)',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)', // emerald-500
|
||||
borderColor: '#10b981',
|
||||
borderWidth: 2,
|
||||
data: [...runningData.value],
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
},
|
||||
{
|
||||
label: '调度中 (Scheduled)',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#6366f1', // indigo-500
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 5],
|
||||
data: [...scheduledData.value],
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
},
|
||||
{
|
||||
label: '排队积压 (Queue)',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#f59e0b', // amber-500
|
||||
borderWidth: 2,
|
||||
borderDash: [2, 2],
|
||||
data: [...queueData.value],
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10
|
||||
}
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabs v-model="activeTab" class="space-y-6 w-full">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">系统监控</h2>
|
||||
<p class="text-muted-foreground text-sm">实时监控面板资源、内存分配和垃圾回收状态</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto">
|
||||
<TabsList class="grid flex-1 sm:flex-none sm:w-[240px] grid-cols-2 h-9 p-1">
|
||||
<TabsTrigger value="charts" class="text-xs sm:text-sm">实时图表</TabsTrigger>
|
||||
<TabsTrigger value="dashboard" class="text-xs sm:text-sm">数据视图</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="() => { disconnectWS(); connectWS() }" :disabled="loading" title="刷新并重连">
|
||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="dashboard" class="mt-0 space-y-4">
|
||||
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-blue-600">执行环境</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">Go 版本</dt><dd class="font-medium">{{ stats.env.go_version }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">系统 / 架构</dt><dd class="font-medium">{{ stats.env.os }} / {{ stats.env.arch }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">逻辑 CPU 数量</dt><dd class="font-medium">{{ stats.env.num_cpu }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前协程数 (Goroutines)</dt><dd class="font-bold text-blue-600">{{ stats.env.goroutines }}</dd></div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-indigo-600">任务调度</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">驻留常驻任务 (Scheduled)</dt><dd class="font-medium text-indigo-600">{{ stats.scheduler.scheduled }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前运行中 (Running)</dt><dd class="font-bold text-emerald-600">{{ stats.scheduler.running }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">排队积压 (Queue Size)</dt><dd class="font-bold text-amber-500">{{ stats.scheduler.queue_size }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">并发池限制 (Worker Count)</dt><dd class="font-medium">{{ stats.scheduler.worker_count }}</dd></div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="stats && stats.scheduler.workers" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-amber-600">并发池 Worker 状态</CardTitle>
|
||||
<CardDescription>精确监控底层协程池调度执行情况</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mt-2">
|
||||
<div v-for="worker in stats.scheduler.workers" :key="worker.id" class="border rounded-md p-3 flex flex-col justify-between" :class="[worker.status === 'running' ? 'bg-amber-50 border-amber-200 dark:bg-amber-950/20 dark:border-amber-900/50' : 'bg-green-50 border-green-200 dark:bg-green-950/20 dark:border-green-900/50']">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<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">
|
||||
🟢 空闲 (Idle)
|
||||
</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">
|
||||
⚡ 执行中 (Running)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="worker.status === 'running'" class="text-xs text-muted-foreground mt-1 space-y-1">
|
||||
<div class="truncate" :title="worker.task_name"><span class="font-medium text-foreground">任务:</span> {{ worker.task_name || worker.task_id }}</div>
|
||||
<div><span class="font-medium text-foreground">运行时间:</span> {{ worker.duration || 0 }}s</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-muted-foreground mt-1">
|
||||
当前暂无任务分配
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-emerald-600">内存概览</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">当前分配内存 (Alloc)</dt><dd class="font-bold text-emerald-600">{{ formatBytes(stats.mem.alloc) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">累计分配内存 (TotalAlloc)</dt><dd class="font-medium">{{ formatBytes(stats.mem.total_alloc) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">向系统获取内存 (Sys)</dt><dd class="font-medium">{{ formatBytes(stats.mem.sys) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">内存分配次数 (Mallocs)</dt><dd class="font-medium">{{ stats.mem.mallocs }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">内存释放次数 (Frees)</dt><dd class="font-medium">{{ stats.mem.frees }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">指针查找次数 (Lookups)</dt><dd class="font-medium">{{ stats.mem.lookups }}</dd></div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-purple-600">堆栈明细</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆分配内存 (HeapAlloc)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_alloc) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆系统内存 (HeapSys)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_sys) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">空闲堆内存 (HeapIdle)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_idle) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">使用中堆内存 (HeapInuse)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_inuse) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">已释放堆内存 (HeapReleased)</dt><dd class="font-medium">{{ formatBytes(stats.heap.heap_released) }}</dd></div>
|
||||
<div class="flex justify-between border-b border-border/50 pb-1"><dt class="text-muted-foreground">堆对象数量 (HeapObjects)</dt><dd class="font-medium">{{ stats.heap.heap_objects }}</dd></div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base text-orange-600">垃圾回收 (GC)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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 时间 (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">执行次数 (NumGC)</dt><dd class="font-medium">{{ stats.gc.num_gc }}</dd></div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="charts" class="mt-0 space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- Scheduler 图表 -->
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-base text-indigo-600">任务调度 (Scheduler)</CardTitle>
|
||||
<CardDescription>跟踪当前节点上正在运行和调度的任务数量</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="h-64 w-full">
|
||||
<Line :data="schedulerChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Goroutines 图表 -->
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-base text-blue-600">并发协程 (Goroutines)</CardTitle>
|
||||
<CardDescription>跟踪系统中轻量级线程的创建与销毁情况</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="h-64 w-full">
|
||||
<Line :data="goroutineChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Memory 图表 -->
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-base text-emerald-600">内存分配 (Memory Alloc vs Sys)</CardTitle>
|
||||
<CardDescription>真实使用的内存 (Alloc) 与向系统申请的内存上限 (Sys)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="h-64 w-full">
|
||||
<Line :data="memChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- GC 图表 -->
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-base text-orange-600">垃圾回收停顿 (GC Pauses)</CardTitle>
|
||||
<CardDescription>每次探针周期内发生的垃圾回收暂停总耗时(毫秒)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="h-64 w-full">
|
||||
<Bar :data="gcChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</template>
|
||||
Reference in New Issue
Block a user