Files
taskq/x/metrics/metrics.go
hupeh 326f2a371c feat: 优化监控仪表盘 UI
- 添加 appbar 导航栏,支持 Chart/Queues 视图切换
- appbar 切换使用 history API,支持浏览器前进/后退
- 图表视图占满整个可视区域
- queue-modal 共享 appbar 样式
- 修复 queue tab count 字段名大小写问题
- tooltip 跟随鼠标显示在右下方,移除箭头
- 图表 canvas 鼠标样式改为准星
- pause/resume 队列后刷新列表
- example 添加 flag 配置参数
2025-12-10 00:53:30 +08:00

269 lines
6.5 KiB
Go

// Package metrics 提供 taskq 的 Prometheus 指标采集功能
package metrics
import (
"context"
"sync"
"time"
"code.tczkiot.com/wlw/taskq"
"github.com/hibiken/asynq"
"github.com/prometheus/client_golang/prometheus"
"github.com/redis/go-redis/v9"
)
// Options 配置指标采集器的选项
type Options struct {
// Namespace 指标命名空间,默认为 "taskq"
Namespace string
// Interval 采集间隔,默认 15 秒
Interval time.Duration
// Registry Prometheus 注册器,默认使用 prometheus.DefaultRegisterer
Registry prometheus.Registerer
}
// Metrics Prometheus 指标采集器
// 实现 taskq.Plugin 接口
type Metrics struct {
opts Options
rdb redis.UniversalClient
queues func() map[string]int
inspector *asynq.Inspector
closeCh chan struct{}
closeOnce sync.Once
// Prometheus 指标
queueSize *prometheus.GaugeVec
activeTasks *prometheus.GaugeVec
pendingTasks *prometheus.GaugeVec
scheduledTasks *prometheus.GaugeVec
retryTasks *prometheus.GaugeVec
archivedTasks *prometheus.GaugeVec
completedTasks *prometheus.GaugeVec
processedTotal *prometheus.GaugeVec
failedTotal *prometheus.GaugeVec
latencySeconds *prometheus.GaugeVec
memoryBytes *prometheus.GaugeVec
paused *prometheus.GaugeVec
}
// New 创建新的指标采集器
func New(opts Options) *Metrics {
if opts.Namespace == "" {
opts.Namespace = "taskq"
}
if opts.Interval <= 0 {
opts.Interval = 15 * time.Second
}
if opts.Registry == nil {
opts.Registry = prometheus.DefaultRegisterer
}
m := &Metrics{
opts: opts,
closeCh: make(chan struct{}),
}
// 初始化指标
m.initMetrics()
return m
}
// initMetrics 初始化 Prometheus 指标
func (m *Metrics) initMetrics() {
labels := []string{"queue"}
m.queueSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "queue_size",
Help: "Total number of tasks in the queue",
}, labels)
m.activeTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_active",
Help: "Number of currently active tasks",
}, labels)
m.pendingTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_pending",
Help: "Number of pending tasks",
}, labels)
m.scheduledTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_scheduled",
Help: "Number of scheduled tasks",
}, labels)
m.retryTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_retry",
Help: "Number of tasks in retry queue",
}, labels)
m.archivedTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_archived",
Help: "Number of archived (dead) tasks",
}, labels)
m.completedTasks = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_completed",
Help: "Number of completed tasks (retained)",
}, labels)
m.processedTotal = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_processed_total",
Help: "Total number of processed tasks (today)",
}, labels)
m.failedTotal = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "tasks_failed_total",
Help: "Total number of failed tasks (today)",
}, labels)
m.latencySeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "queue_latency_seconds",
Help: "Queue latency in seconds",
}, labels)
m.memoryBytes = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "queue_memory_bytes",
Help: "Memory usage of the queue in bytes",
}, labels)
m.paused = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: m.opts.Namespace,
Name: "queue_paused",
Help: "Whether the queue is paused (1) or not (0)",
}, labels)
}
// Name 返回插件名称
func (m *Metrics) Name() string {
return "metrics"
}
// Init 初始化插件,从 Context 获取 Redis 和 Queues
func (m *Metrics) Init(ctx *taskq.Context) error {
m.rdb = ctx.Redis()
m.queues = ctx.Queues
return nil
}
// Start 启动指标采集
func (m *Metrics) Start(ctx *taskq.Context) error {
m.inspector = asynq.NewInspectorFromRedisClient(m.rdb)
// 注册指标
collectors := []prometheus.Collector{
m.queueSize,
m.activeTasks,
m.pendingTasks,
m.scheduledTasks,
m.retryTasks,
m.archivedTasks,
m.completedTasks,
m.processedTotal,
m.failedTotal,
m.latencySeconds,
m.memoryBytes,
m.paused,
}
for _, c := range collectors {
if err := m.opts.Registry.Register(c); err != nil {
// 如果已注册则忽略
if _, ok := err.(prometheus.AlreadyRegisteredError); !ok {
return err
}
}
}
// 启动后台采集
go m.startCollector(ctx)
return nil
}
// Stop 停止指标采集
func (m *Metrics) Stop() error {
m.closeOnce.Do(func() {
close(m.closeCh)
})
if m.inspector != nil {
m.inspector.Close()
}
return nil
}
// startCollector 启动后台指标采集
func (m *Metrics) startCollector(ctx context.Context) {
// 立即采集一次
m.collect()
ticker := time.NewTicker(m.opts.Interval)
defer ticker.Stop()
for {
select {
case <-m.closeCh:
return
case <-ctx.Done():
return
case <-ticker.C:
m.collect()
}
}
}
// collect 采集所有队列的指标
func (m *Metrics) collect() {
if m.queues == nil || m.inspector == nil {
return
}
queues := m.queues()
for queueName := range queues {
info, err := m.inspector.GetQueueInfo(queueName)
if err != nil {
continue
}
m.queueSize.WithLabelValues(queueName).Set(float64(info.Size))
m.activeTasks.WithLabelValues(queueName).Set(float64(info.Active))
m.pendingTasks.WithLabelValues(queueName).Set(float64(info.Pending))
m.scheduledTasks.WithLabelValues(queueName).Set(float64(info.Scheduled))
m.retryTasks.WithLabelValues(queueName).Set(float64(info.Retry))
m.archivedTasks.WithLabelValues(queueName).Set(float64(info.Archived))
m.completedTasks.WithLabelValues(queueName).Set(float64(info.Completed))
m.processedTotal.WithLabelValues(queueName).Set(float64(info.Processed))
m.failedTotal.WithLabelValues(queueName).Set(float64(info.Failed))
m.latencySeconds.WithLabelValues(queueName).Set(info.Latency.Seconds())
m.memoryBytes.WithLabelValues(queueName).Set(float64(info.MemoryUsage))
if info.Paused {
m.paused.WithLabelValues(queueName).Set(1)
} else {
m.paused.WithLabelValues(queueName).Set(0)
}
}
}