2025-12-09 14:31:02 +08:00
|
|
|
|
// Package taskq 提供基于 Redis 的异步任务队列功能
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// inspect.go 文件包含统计采集器和相关数据结构
|
2025-12-09 14:31:02 +08:00
|
|
|
|
package taskq
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-09 19:58:18 +08:00
|
|
|
|
"database/sql"
|
2025-12-09 14:31:02 +08:00
|
|
|
|
"fmt"
|
2025-12-09 19:58:18 +08:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2025-12-09 14:31:02 +08:00
|
|
|
|
"strings"
|
2025-12-09 19:58:18 +08:00
|
|
|
|
"sync"
|
2025-12-09 14:31:02 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/hibiken/asynq"
|
2025-12-09 19:58:18 +08:00
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
2025-12-09 14:31:02 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ==================== Inspector 统计采集器 ====================
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// Inspector 统计采集器,独立于 HTTP 服务运行
|
|
|
|
|
|
type Inspector struct {
|
|
|
|
|
|
inspector *asynq.Inspector
|
|
|
|
|
|
db *sql.DB
|
|
|
|
|
|
closeCh chan struct{}
|
|
|
|
|
|
closeOnce sync.Once
|
|
|
|
|
|
interval time.Duration
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// InspectorOptions 配置统计采集器的选项
|
|
|
|
|
|
type InspectorOptions struct {
|
|
|
|
|
|
// Interval 采集间隔,默认 2 秒
|
|
|
|
|
|
Interval time.Duration
|
|
|
|
|
|
|
|
|
|
|
|
// DBPath SQLite 数据库文件路径,默认为 "./taskq_stats.db"
|
|
|
|
|
|
DBPath string
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// NewInspector 创建新的统计采集器
|
|
|
|
|
|
func NewInspector(opts InspectorOptions) (*Inspector, error) {
|
2025-12-09 14:31:02 +08:00
|
|
|
|
if redisClient == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("taskq: redis client not initialized, call SetRedis() first")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
if opts.Interval <= 0 {
|
|
|
|
|
|
opts.Interval = 2 * time.Second
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
if opts.DBPath == "" {
|
|
|
|
|
|
opts.DBPath = "./taskq_stats.db"
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// 确保目录存在
|
|
|
|
|
|
dir := filepath.Dir(opts.DBPath)
|
|
|
|
|
|
if dir != "" && dir != "." {
|
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("taskq: failed to create directory: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// 打开 SQLite 数据库
|
|
|
|
|
|
db, err := sql.Open("sqlite3", opts.DBPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("taskq: failed to open database: %v", err)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// 初始化数据库表
|
|
|
|
|
|
if err := initStatsDB(db); err != nil {
|
|
|
|
|
|
db.Close()
|
|
|
|
|
|
return nil, fmt.Errorf("taskq: failed to init database: %v", err)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
ins := &Inspector{
|
|
|
|
|
|
inspector: asynq.NewInspectorFromRedisClient(redisClient),
|
|
|
|
|
|
db: db,
|
|
|
|
|
|
closeCh: make(chan struct{}),
|
|
|
|
|
|
interval: opts.Interval,
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// 启动后台统计采集
|
|
|
|
|
|
go ins.startCollector()
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
return ins, nil
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// initStatsDB 初始化数据库(Prometheus 风格:单表 + 标签)
|
|
|
|
|
|
// 设计思路:
|
|
|
|
|
|
// - 单表存储所有队列的统计数据,通过 queue 列区分
|
|
|
|
|
|
// - 复合索引支持按时间和队列两个维度高效查询
|
|
|
|
|
|
// - 类似 Prometheus 的 (timestamp, labels, value) 模型
|
|
|
|
|
|
func initStatsDB(db *sql.DB) error {
|
|
|
|
|
|
_, err := db.Exec(`
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS metrics (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
|
timestamp INTEGER NOT NULL,
|
|
|
|
|
|
queue TEXT NOT NULL,
|
|
|
|
|
|
active INTEGER DEFAULT 0,
|
|
|
|
|
|
pending INTEGER DEFAULT 0,
|
|
|
|
|
|
scheduled INTEGER DEFAULT 0,
|
|
|
|
|
|
retry INTEGER DEFAULT 0,
|
|
|
|
|
|
archived INTEGER DEFAULT 0,
|
|
|
|
|
|
completed INTEGER DEFAULT 0,
|
|
|
|
|
|
succeeded INTEGER DEFAULT 0,
|
|
|
|
|
|
failed INTEGER DEFAULT 0
|
|
|
|
|
|
);
|
|
|
|
|
|
-- 按队列查询:WHERE queue = ? ORDER BY timestamp
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_metrics_queue_time ON metrics(queue, timestamp DESC);
|
|
|
|
|
|
-- 按时间查询所有队列:WHERE timestamp BETWEEN ? AND ?
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_metrics_time ON metrics(timestamp DESC);
|
|
|
|
|
|
-- 唯一约束:同一时间同一队列只有一条记录
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_metrics_unique ON metrics(timestamp, queue);
|
|
|
|
|
|
`)
|
|
|
|
|
|
return err
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// Close 关闭统计采集器
|
|
|
|
|
|
func (ins *Inspector) Close() error {
|
|
|
|
|
|
ins.closeOnce.Do(func() {
|
|
|
|
|
|
close(ins.closeCh)
|
|
|
|
|
|
})
|
|
|
|
|
|
if ins.db != nil {
|
|
|
|
|
|
ins.db.Close()
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
2025-12-09 19:58:18 +08:00
|
|
|
|
return ins.inspector.Close()
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// startCollector 启动后台统计采集任务
|
|
|
|
|
|
func (ins *Inspector) startCollector() {
|
|
|
|
|
|
ticker := time.NewTicker(ins.interval)
|
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ins.closeCh:
|
|
|
|
|
|
return
|
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
|
ins.collectStats()
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
2025-12-09 19:58:18 +08:00
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// collectStats 采集所有队列的统计数据
|
|
|
|
|
|
func (ins *Inspector) collectStats() {
|
|
|
|
|
|
now := time.Now().Unix()
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
for queueName := range queues {
|
|
|
|
|
|
stats, err := ins.inspector.GetQueueInfo(queueName)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
if err != nil {
|
2025-12-09 19:58:18 +08:00
|
|
|
|
continue
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
qs := QueueStats{
|
|
|
|
|
|
Queue: queueName,
|
|
|
|
|
|
Timestamp: now,
|
|
|
|
|
|
Active: stats.Active,
|
|
|
|
|
|
Pending: stats.Pending,
|
|
|
|
|
|
Scheduled: stats.Scheduled,
|
|
|
|
|
|
Retry: stats.Retry,
|
|
|
|
|
|
Archived: stats.Archived,
|
|
|
|
|
|
Completed: stats.Completed,
|
|
|
|
|
|
Succeeded: stats.Processed - stats.Failed,
|
|
|
|
|
|
Failed: stats.Failed,
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
ins.saveMetrics(qs)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// saveMetrics 保存统计数据到 metrics 表
|
|
|
|
|
|
func (ins *Inspector) saveMetrics(stats QueueStats) error {
|
|
|
|
|
|
if ins.db == nil {
|
|
|
|
|
|
return nil
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
_, err := ins.db.Exec(`
|
|
|
|
|
|
INSERT OR REPLACE INTO metrics (timestamp, queue, active, pending, scheduled, retry, archived, completed, succeeded, failed)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
`, stats.Timestamp, stats.Queue, stats.Active, stats.Pending, stats.Scheduled, stats.Retry, stats.Archived, stats.Completed, stats.Succeeded, stats.Failed)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// GetQueueInfo 获取队列信息
|
|
|
|
|
|
func (ins *Inspector) GetQueueInfo(queueName string) (*asynq.QueueInfo, error) {
|
|
|
|
|
|
return ins.inspector.GetQueueInfo(queueName)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListActiveTasks 获取活跃任务列表
|
|
|
|
|
|
func (ins *Inspector) ListActiveTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListActiveTasks(queueName, opts...)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListPendingTasks 获取等待任务列表
|
|
|
|
|
|
func (ins *Inspector) ListPendingTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListPendingTasks(queueName, opts...)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListScheduledTasks 获取计划任务列表
|
|
|
|
|
|
func (ins *Inspector) ListScheduledTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListScheduledTasks(queueName, opts...)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListRetryTasks 获取重试任务列表
|
|
|
|
|
|
func (ins *Inspector) ListRetryTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListRetryTasks(queueName, opts...)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListArchivedTasks 获取归档任务列表
|
|
|
|
|
|
func (ins *Inspector) ListArchivedTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListArchivedTasks(queueName, opts...)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ListCompletedTasks 获取已完成任务列表
|
|
|
|
|
|
func (ins *Inspector) ListCompletedTasks(queueName string, opts ...asynq.ListOption) ([]*asynq.TaskInfo, error) {
|
|
|
|
|
|
return ins.inspector.ListCompletedTasks(queueName, opts...)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// RunTask 立即运行归档任务(重试失败任务)
|
|
|
|
|
|
func (ins *Inspector) RunTask(queueName, taskID string) error {
|
|
|
|
|
|
return ins.inspector.RunTask(queueName, taskID)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// PauseQueue 暂停队列
|
|
|
|
|
|
func (ins *Inspector) PauseQueue(queueName string) error {
|
|
|
|
|
|
return ins.inspector.PauseQueue(queueName)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// UnpauseQueue 恢复队列
|
|
|
|
|
|
func (ins *Inspector) UnpauseQueue(queueName string) error {
|
|
|
|
|
|
return ins.inspector.UnpauseQueue(queueName)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// ==================== 统计数据结构 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// QueueInfo 获取每个队列的详细信息
|
|
|
|
|
|
type QueueInfo struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Priority int `json:"priority"`
|
|
|
|
|
|
Size int `json:"size"` // 队列中任务总数
|
|
|
|
|
|
Active int `json:"active"` // 活跃任务数
|
|
|
|
|
|
Pending int `json:"pending"` // 等待任务数
|
|
|
|
|
|
Scheduled int `json:"scheduled"` // 计划任务数
|
|
|
|
|
|
Retry int `json:"retry"` // 重试任务数
|
|
|
|
|
|
Archived int `json:"archived"` // 归档任务数
|
|
|
|
|
|
Completed int `json:"completed"` // 已完成任务数
|
|
|
|
|
|
Processed int `json:"processed"` // 累计处理数(今日)
|
|
|
|
|
|
Failed int `json:"failed"` // 累计失败数(今日)
|
|
|
|
|
|
Paused bool `json:"paused"` // 是否暂停
|
|
|
|
|
|
MemoryUsage int64 `json:"memory_usage"` // 内存使用(字节)
|
|
|
|
|
|
Latency int64 `json:"latency"` // 延迟(毫秒)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// QueueStats 队列统计数据点(用于存储历史数据)
|
|
|
|
|
|
type QueueStats struct {
|
|
|
|
|
|
Timestamp int64 `json:"t"` // Unix 时间戳(秒)
|
|
|
|
|
|
Queue string `json:"q,omitempty"` // 队列名称(汇总查询时为空)
|
|
|
|
|
|
Active int `json:"a"` // 活跃任务数
|
|
|
|
|
|
Pending int `json:"p"` // 等待任务数
|
|
|
|
|
|
Scheduled int `json:"s"` // 计划任务数
|
|
|
|
|
|
Retry int `json:"r"` // 重试任务数
|
|
|
|
|
|
Archived int `json:"ar"` // 归档任务数
|
|
|
|
|
|
Completed int `json:"c"` // 已完成任务数
|
|
|
|
|
|
Succeeded int `json:"su"` // 成功数
|
|
|
|
|
|
Failed int `json:"f"` // 失败数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 全局统计数据查询 ====================
|
|
|
|
|
|
|
|
|
|
|
|
var statsDB *sql.DB
|
|
|
|
|
|
var statsDBMu sync.RWMutex
|
|
|
|
|
|
|
|
|
|
|
|
// SetStatsDB 设置全局统计数据库(供 HTTPHandler 使用)
|
|
|
|
|
|
func SetStatsDB(db *sql.DB) {
|
|
|
|
|
|
statsDBMu.Lock()
|
|
|
|
|
|
defer statsDBMu.Unlock()
|
|
|
|
|
|
statsDB = db
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// StatsQuery 统计查询参数
|
|
|
|
|
|
type StatsQuery struct {
|
|
|
|
|
|
Queue string // 队列名称,为空则查询所有队列汇总
|
|
|
|
|
|
Start int64 // 开始时间戳(秒),0 表示不限制
|
|
|
|
|
|
End int64 // 结束时间戳(秒),0 表示不限制
|
|
|
|
|
|
Limit int // 返回数量限制,默认 500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getQueueStats 获取队列历史统计数据
|
|
|
|
|
|
func getQueueStats(queueName string, limit int) ([]QueueStats, error) {
|
|
|
|
|
|
return getQueueStatsWithQuery(StatsQuery{
|
|
|
|
|
|
Queue: queueName,
|
|
|
|
|
|
Limit: limit,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getQueueStatsWithQuery 根据查询条件获取统计数据(Prometheus 风格单表查询)
|
|
|
|
|
|
// - 按队列查询:使用 idx_metrics_queue_time 索引
|
|
|
|
|
|
// - 按时间汇总:使用 idx_metrics_time 索引 + GROUP BY
|
|
|
|
|
|
func getQueueStatsWithQuery(q StatsQuery) ([]QueueStats, error) {
|
|
|
|
|
|
statsDBMu.RLock()
|
|
|
|
|
|
db := statsDB
|
|
|
|
|
|
statsDBMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if db == nil {
|
|
|
|
|
|
return nil, nil
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
if q.Limit <= 0 {
|
|
|
|
|
|
q.Limit = 500
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
var args []any
|
|
|
|
|
|
var whereClause string
|
|
|
|
|
|
var conditions []string
|
|
|
|
|
|
|
|
|
|
|
|
// 构建 WHERE 条件
|
|
|
|
|
|
if q.Queue != "" {
|
|
|
|
|
|
conditions = append(conditions, "queue = ?")
|
|
|
|
|
|
args = append(args, q.Queue)
|
|
|
|
|
|
}
|
|
|
|
|
|
if q.Start > 0 {
|
|
|
|
|
|
conditions = append(conditions, "timestamp >= ?")
|
|
|
|
|
|
args = append(args, q.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
if q.End > 0 {
|
|
|
|
|
|
conditions = append(conditions, "timestamp <= ?")
|
|
|
|
|
|
args = append(args, q.End)
|
|
|
|
|
|
}
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
if len(conditions) > 0 {
|
|
|
|
|
|
whereClause = "WHERE " + strings.Join(conditions, " AND ")
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
var query string
|
|
|
|
|
|
if q.Queue != "" {
|
|
|
|
|
|
// 查询单个队列
|
|
|
|
|
|
query = fmt.Sprintf(`
|
|
|
|
|
|
SELECT timestamp, queue, active, pending, scheduled, retry, archived, completed, succeeded, failed
|
|
|
|
|
|
FROM metrics
|
|
|
|
|
|
%s
|
|
|
|
|
|
ORDER BY timestamp DESC
|
|
|
|
|
|
LIMIT ?
|
|
|
|
|
|
`, whereClause)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 查询所有队列汇总(按时间 GROUP BY)
|
|
|
|
|
|
query = fmt.Sprintf(`
|
|
|
|
|
|
SELECT timestamp, '' as queue, SUM(active), SUM(pending), SUM(scheduled), SUM(retry), SUM(archived), SUM(completed), SUM(succeeded), SUM(failed)
|
|
|
|
|
|
FROM metrics
|
|
|
|
|
|
%s
|
|
|
|
|
|
GROUP BY timestamp
|
|
|
|
|
|
ORDER BY timestamp DESC
|
|
|
|
|
|
LIMIT ?
|
|
|
|
|
|
`, whereClause)
|
|
|
|
|
|
}
|
|
|
|
|
|
args = append(args, q.Limit)
|
|
|
|
|
|
|
|
|
|
|
|
rows, err := db.Query(query, args...)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
if err != nil {
|
2025-12-09 19:58:18 +08:00
|
|
|
|
return nil, err
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
2025-12-09 19:58:18 +08:00
|
|
|
|
defer rows.Close()
|
2025-12-09 14:31:02 +08:00
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
var statsList []QueueStats
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
|
var s QueueStats
|
|
|
|
|
|
if err := rows.Scan(&s.Timestamp, &s.Queue, &s.Active, &s.Pending, &s.Scheduled, &s.Retry, &s.Archived, &s.Completed, &s.Succeeded, &s.Failed); err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
statsList = append(statsList, s)
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 19:58:18 +08:00
|
|
|
|
// 反转顺序,使时间从早到晚
|
|
|
|
|
|
for i, j := 0, len(statsList)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
|
|
statsList[i], statsList[j] = statsList[j], statsList[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return statsList, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetStatsDB 返回 Inspector 的数据库连接(供外部设置给 HTTPHandler)
|
|
|
|
|
|
func (ins *Inspector) GetStatsDB() *sql.DB {
|
|
|
|
|
|
return ins.db
|
2025-12-09 14:31:02 +08:00
|
|
|
|
}
|