feat: 优化监控仪表盘 UI

- 添加 appbar 导航栏,支持 Chart/Queues 视图切换
- appbar 切换使用 history API,支持浏览器前进/后退
- 图表视图占满整个可视区域
- queue-modal 共享 appbar 样式
- 修复 queue tab count 字段名大小写问题
- tooltip 跟随鼠标显示在右下方,移除箭头
- 图表 canvas 鼠标样式改为准星
- pause/resume 队列后刷新列表
- example 添加 flag 配置参数
This commit is contained in:
2025-12-10 00:53:30 +08:00
parent 1f9f1cab53
commit 326f2a371c
19 changed files with 1626 additions and 909 deletions

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
@@ -11,7 +12,12 @@ import (
"time"
"code.tczkiot.com/wlw/taskq"
"code.tczkiot.com/wlw/taskq/x/inspector"
"code.tczkiot.com/wlw/taskq/x/metrics"
"code.tczkiot.com/wlw/taskq/x/monitor"
_ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9"
)
@@ -28,83 +34,99 @@ type ImageResizeTask struct {
// 定义任务处理器
func handleEmailTask(ctx context.Context, t EmailTask) error {
log.Printf("处理邮件任务: 用户ID=%d, 模板ID=%s", t.UserID, t.TemplateID)
// 模拟邮件发送逻辑
return nil
}
func handleImageResizeTask(ctx context.Context, t ImageResizeTask) error {
log.Printf("处理图片调整任务: 源URL=%s", t.SourceURL)
// 模拟图片调整逻辑
return nil
}
var (
redisAddr = flag.String("redis", "127.0.0.1:6379", "Redis 地址")
redisDB = flag.Int("redis-db", 1, "Redis 数据库")
httpAddr = flag.String("http", ":8081", "HTTP 服务地址")
dbPath = flag.String("db", "./taskq_stats.db", "SQLite 数据库路径")
)
func main() {
flag.Parse()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
DB: 1,
Addr: *redisAddr,
DB: *redisDB,
})
defer rdb.Close()
// 初始化 taskq
taskq.SetRedis(rdb)
taskq.Init()
// 创建邮件任务
emailTask := &taskq.Task[EmailTask]{
emailTask := &taskq.Task{
Queue: "email",
Name: "email:deliver",
MaxRetries: 3,
Priority: 5,
TTR: 0,
Handler: handleEmailTask,
}
// 创建图片调整任务
imageTask := &taskq.Task[ImageResizeTask]{
imageTask := &taskq.Task{
Queue: "image",
Name: "image:resize",
MaxRetries: 3,
Priority: 3,
TTR: 0,
Handler: handleImageResizeTask,
}
// 注册任务
if err := taskq.Register(emailTask); err != nil {
log.Fatal("注册邮件任务失败:", err)
}
if err := taskq.Register(imageTask); err != nil {
log.Fatal("注册图片任务失败:", err)
// 创建 Inspector 插件(用于监控仪表盘)
ins := inspector.New(inspector.Options{
Interval: 2 * time.Second,
DBPath: *dbPath,
})
// 创建 Metrics 插件(用于 Prometheus
met := metrics.New(metrics.Options{
Namespace: "taskq",
Interval: 15 * time.Second,
})
// 配置 taskq
if err := taskq.Configure(taskq.Config{
Redis: rdb,
Tasks: []*taskq.Task{emailTask, imageTask},
Plugins: []taskq.Plugin{ins, met},
}); err != nil {
log.Fatal("配置 taskq 失败:", err)
}
// 创建监控 HTTP 处理器
handler, err := taskq.NewHTTPHandler(taskq.HTTPHandlerOptions{
RootPath: "/monitor",
ReadOnly: false,
// 创建监控服务
servlet := taskq.Default()
mon, err := monitor.New(monitor.Options{
Inspector: ins,
Queues: servlet.Queues(),
RootPath: "/monitor",
ReadOnly: false,
})
if err != nil {
log.Fatal("创建监控处理器失败:", err)
log.Fatal("创建监控服务失败:", err)
}
// 创建可取消的 context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 启动 taskq 服务器(包含统计采集器
go func() {
err := taskq.Start(ctx, taskq.StartOptions{
StatsInterval: 2 * time.Second,
StatsDBPath: "./taskq_stats.db",
})
if err != nil {
log.Fatal("启动 taskq 服务器失败:", err)
}
}()
// 初始化 taskq(初始化所有插件
if err := taskq.Init(ctx); err != nil {
log.Fatal("初始化 taskq 失败:", err)
}
// 启动 taskq 服务器(启动所有插件)
if err := taskq.Start(ctx); err != nil {
log.Fatal("启动 taskq 服务器失败:", err)
}
// 定时发布任务
go func() {
ticker := time.NewTicker(5 * time.Second) // 每5秒发布一次任务
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
taskCounter := 0
@@ -162,15 +184,22 @@ func main() {
}
}()
// 创建 HTTP 路由
mux := http.NewServeMux()
mux.Handle("/monitor/", mon)
mux.Handle("/metrics", promhttp.Handler())
// 创建 HTTP 服务器
server := &http.Server{
Addr: ":8081",
Handler: handler,
Addr: *httpAddr,
Handler: mux,
}
// 启动 HTTP 服务器(非阻塞)
// 启动 HTTP 服务器
go func() {
log.Printf("启动监控服务器在 http://localhost:8081")
log.Printf("启动服务器在 http://localhost%s", *httpAddr)
log.Printf(" - 监控仪表盘: http://localhost%s/monitor", *httpAddr)
log.Printf(" - Prometheus: http://localhost%s/metrics", *httpAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("HTTP 服务器错误:", err)
}
@@ -186,10 +215,10 @@ func main() {
// 1. 取消 context停止任务发布
cancel()
// 2. 关闭监控 HTTP 处理器(会断开 SSE 连接)
handler.Close()
// 2. 关闭监控服务(断开 SSE 连接)
mon.Close()
// 3. 关闭 HTTP 服务器(设置 5 秒超时)
// 3. 关闭 HTTP 服务器
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
@@ -197,9 +226,8 @@ func main() {
log.Printf("HTTP 服务器关闭错误: %v", err)
}
// 4. 停止 taskq 服务器(会等待完全关闭
// 4. 停止 taskq 服务器(会自动调用插件的 OnStop
taskq.Stop()
log.Println("服务已安全关闭")
// rdb.Close() 由 defer 执行
}