feat: Redis热数据+K线聚合+Dashboard MySQL指标修复
This commit is contained in:
1 parent
b8b1a3d5f6
commit
a843ab7b00
9 files changed
+311
-12
No files matched your search
@@ -3,22 +3,28 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ezops/internal/models"
|
"ezops/internal/models"
|
||||||
|
ezredis "ezops/pkg/redis"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentHandler 处理 Agent 上报数据 // Logan.Li
|
// AgentHandler 处理 Agent 上报数据 // Logan.Li
|
||||||
type AgentHandler struct {
|
type AgentHandler struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
cache ezredis.Cache // 可选 Redis 缓存(用于热数据存储)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentHandler(db *gorm.DB) *AgentHandler {
|
func NewAgentHandler(db *gorm.DB, cache ezredis.Cache) *AgentHandler {
|
||||||
return &AgentHandler{db: db}
|
return &AgentHandler{db: db, cache: cache}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceiveReport 接收 Agent 上报的指标数据
|
// ReceiveReport 接收 Agent 上报的指标数据
|
||||||
@@ -63,10 +69,44 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入 Redis 有序集合(热数据,保留7天)
|
||||||
|
if h.cache != nil {
|
||||||
|
timestamp := float64(reportTime.Unix())
|
||||||
|
ttl := 7 * 24 * time.Hour // 7天
|
||||||
|
|
||||||
|
// 使用 reportTime.UnixNano() 确保 member 唯一(避免相同值碰撞)
|
||||||
|
h.writeHotMetric(c, "metrics:cpu:"+req.Hostname, req.CPU.UsagePercent, timestamp, ttl, reportTime)
|
||||||
|
h.writeHotMetric(c, "metrics:mem:"+req.Hostname, req.Memory.UsagePercent, timestamp, ttl, reportTime)
|
||||||
|
h.writeHotMetric(c, "metrics:load1:"+req.Hostname, req.CPU.Load1, timestamp, ttl, reportTime)
|
||||||
|
}
|
||||||
|
|
||||||
// 保存成功
|
// 保存成功
|
||||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeHotMetric 将单个指标写入 Redis 有序集合并维护 TTL 和过期数据清理
|
||||||
|
func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64, score float64, ttl time.Duration, reportTime time.Time) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// member 格式: "value:nano_timestamp" 保证唯一性
|
||||||
|
member := fmt.Sprintf("%.4f:%d", value, reportTime.UnixNano())
|
||||||
|
|
||||||
|
if err := h.cache.ZAdd(ctx, key, ezredis.SortedSetMember{
|
||||||
|
Score: score,
|
||||||
|
Member: member,
|
||||||
|
}); err != nil {
|
||||||
|
// 静默失败,不影响主流程
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 key 过期时间
|
||||||
|
_ = h.cache.Expire(ctx, key, ttl)
|
||||||
|
|
||||||
|
// 清理 7 天前的旧数据
|
||||||
|
cutoff := float64(time.Now().Add(-ttl).Unix())
|
||||||
|
_ = h.cache.ZRemRangeByScore(ctx, key, "-inf", fmt.Sprintf("%.0f", cutoff))
|
||||||
|
}
|
||||||
|
|
||||||
// GetLatestReports 获取各主机最新上报
|
// GetLatestReports 获取各主机最新上报
|
||||||
// GET /api/v1/agent/latest
|
// GET /api/v1/agent/latest
|
||||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||||
@@ -108,3 +148,160 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
|||||||
Data: reports,
|
Data: reports,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AggregateMetrics 聚合 Redis 热数据到 MySQL klines
|
||||||
|
// POST /api/v1/agent/aggregate
|
||||||
|
func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||||
|
var req models.AggregateKlineReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cache == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, models.Response{Code: 503, Message: "Redis 缓存不可用"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持的聚合间隔及对应的回溯时长
|
||||||
|
intervals := []struct {
|
||||||
|
name string
|
||||||
|
duration time.Duration
|
||||||
|
}{
|
||||||
|
{"1m", 1 * time.Minute},
|
||||||
|
{"3m", 3 * time.Minute},
|
||||||
|
{"1h", 1 * time.Hour},
|
||||||
|
{"6h", 6 * time.Hour},
|
||||||
|
{"1d", 24 * time.Hour},
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKey := fmt.Sprintf("metrics:%s:%s", req.Metric, req.Hostname)
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
var totalUpserted int64
|
||||||
|
|
||||||
|
for _, iv := range intervals {
|
||||||
|
// 回溯窗口:取 interval × 200 倍,确保覆盖足够多的数据点
|
||||||
|
lookback := iv.duration * 200
|
||||||
|
now := time.Now()
|
||||||
|
since := now.Add(-lookback)
|
||||||
|
minScore := fmt.Sprintf("%d", since.Unix())
|
||||||
|
maxScore := fmt.Sprintf("%d", now.Unix()+1)
|
||||||
|
|
||||||
|
members, err := h.cache.ZRangeByScore(ctx, redisKey, minScore, maxScore)
|
||||||
|
if err != nil || len(members) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按间隔分桶
|
||||||
|
buckets := make(map[int64][]float64) // bucketTime (unix) → values
|
||||||
|
for _, m := range members {
|
||||||
|
ts := int64(m.Score)
|
||||||
|
bucketStart := (ts / int64(iv.duration.Seconds())) * int64(iv.duration.Seconds())
|
||||||
|
val := parseMemberValue(m.Member)
|
||||||
|
buckets[bucketStart] = append(buckets[bucketStart], val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个桶计算 OHLC + Avg
|
||||||
|
for bucketTs, values := range buckets {
|
||||||
|
if len(values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
open := values[0]
|
||||||
|
closeV := values[len(values)-1]
|
||||||
|
high := values[0]
|
||||||
|
low := values[0]
|
||||||
|
sum := 0.0
|
||||||
|
|
||||||
|
for _, v := range values {
|
||||||
|
sum += v
|
||||||
|
if v > high {
|
||||||
|
high = v
|
||||||
|
}
|
||||||
|
if v < low {
|
||||||
|
low = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avg := sum / float64(len(values))
|
||||||
|
|
||||||
|
kline := models.MetricsKline{
|
||||||
|
Hostname: req.Hostname,
|
||||||
|
Metric: req.Metric,
|
||||||
|
Interval: iv.name,
|
||||||
|
Open: math.Round(open*100) / 100,
|
||||||
|
High: math.Round(high*100) / 100,
|
||||||
|
Low: math.Round(low*100) / 100,
|
||||||
|
Close: math.Round(closeV*100) / 100,
|
||||||
|
Avg: math.Round(avg*100) / 100,
|
||||||
|
Count: len(values),
|
||||||
|
BucketTime: time.Unix(bucketTs, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert: hostname + metric + interval + bucket_time 唯一
|
||||||
|
result := h.db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{
|
||||||
|
{Name: "hostname"},
|
||||||
|
{Name: "metric"},
|
||||||
|
{Name: "interval"},
|
||||||
|
{Name: "bucket_time"},
|
||||||
|
},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
|
"open", "high", "low", "close", "avg", "count",
|
||||||
|
}),
|
||||||
|
}).Create(&kline)
|
||||||
|
|
||||||
|
if result.Error == nil {
|
||||||
|
totalUpserted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: fmt.Sprintf("聚合完成,写入/更新 %d 条记录", totalUpserted),
|
||||||
|
Data: gin.H{"upserted": totalUpserted},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKline 查询 K 线数据
|
||||||
|
// GET /api/v1/agent/kline?hostname=&metric=&interval=&from=&to=
|
||||||
|
func (h *AgentHandler) GetKline(c *gin.Context) {
|
||||||
|
var req models.KlineQueryReq
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
from, err := time.Parse(time.RFC3339, req.From)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "from 格式错误,需 RFC3339: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
to, err := time.Parse(time.RFC3339, req.To)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "to 格式错误,需 RFC3339: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var klines []models.MetricsKline
|
||||||
|
h.db.Where("hostname = ? AND metric = ? AND interval = ? AND bucket_time >= ? AND bucket_time <= ?",
|
||||||
|
req.Hostname, req.Metric, req.Interval, from, to).
|
||||||
|
Order("bucket_time ASC").
|
||||||
|
Find(&klines)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: "success",
|
||||||
|
Data: klines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMemberValue 从 member 字符串 "45.2300:1234567890123456789" 中提取浮点值
|
||||||
|
func parseMemberValue(member string) float64 {
|
||||||
|
idx := strings.Index(member, ":")
|
||||||
|
if idx < 0 {
|
||||||
|
v, _ := strconv.ParseFloat(member, 64)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
v, _ := strconv.ParseFloat(member[:idx], 64)
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -294,17 +294,17 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 agent_reports 表查最近 N 小时的真实数据 // Logan.Li
|
// 从 agent_reports 表查最近 N 小时的真实数据(MySQL 聚合)
|
||||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||||
type avgPoint struct {
|
type avgPoint struct {
|
||||||
Bucket string // strftime 返回字符串 "2006-01-02 15:00",不能直接扫入 time.Time(会得零值)
|
Bucket string
|
||||||
AvgCPU float64
|
AvgCPU float64
|
||||||
AvgMem float64
|
AvgMem float64
|
||||||
}
|
}
|
||||||
var rows []avgPoint
|
var rows []avgPoint
|
||||||
h.db.Model(&models.AgentReport{}).
|
h.db.Model(&models.AgentReport{}).
|
||||||
Select("strftime('%Y-%m-%d %H:00', created_at) as bucket, AVG(cpu_usage) as avg_cpu, AVG(mem_usage) as avg_mem").
|
Select("DATE_FORMAT(report_time, '%Y-%m-%d %H:00') as bucket, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem").
|
||||||
Where("created_at >= ?", since).
|
Where("report_time >= ?", since).
|
||||||
Group("bucket").
|
Group("bucket").
|
||||||
Order("bucket ASC").
|
Order("bucket ASC").
|
||||||
Scan(&rows)
|
Scan(&rows)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"ezops/internal/api/handlers"
|
"ezops/internal/api/handlers"
|
||||||
"ezops/internal/api/middleware"
|
"ezops/internal/api/middleware"
|
||||||
"ezops/internal/config"
|
"ezops/internal/config"
|
||||||
|
ezredis "ezops/pkg/redis"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -63,6 +64,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
api.POST("/auth/login", authHandler.Login)
|
api.POST("/auth/login", authHandler.Login)
|
||||||
api.POST("/auth/register", authHandler.Register)
|
api.POST("/auth/register", authHandler.Register)
|
||||||
|
|
||||||
|
// Agent 安装处理器(在 servers 和 agent 路由中共享)
|
||||||
|
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
||||||
|
|
||||||
// 需要认证的路由
|
// 需要认证的路由
|
||||||
authorized := api.Group("")
|
authorized := api.Group("")
|
||||||
authorized.Use(middleware.JWTAuth(&cfg.JWT))
|
authorized.Use(middleware.JWTAuth(&cfg.JWT))
|
||||||
@@ -139,12 +143,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
servers.POST("/batch", serverHandler.BatchCreateServers)
|
servers.POST("/batch", serverHandler.BatchCreateServers)
|
||||||
|
|
||||||
// Agent 远程安装 // Logan.Li
|
// Agent 远程安装 // Logan.Li
|
||||||
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
|
||||||
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
||||||
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
|
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
|
||||||
|
|
||||||
// Agent 远程命令执行
|
|
||||||
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 云账号路由
|
// 云账号路由
|
||||||
@@ -413,19 +413,23 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agent 上报路由(无需 JWT)
|
// Agent 上报路由(无需 JWT)
|
||||||
agentHandler := handlers.NewAgentHandler(db)
|
redisCache, _ := cache.(ezredis.Cache)
|
||||||
|
agentHandler := handlers.NewAgentHandler(db, redisCache)
|
||||||
taskHandler := handlers.NewAgentTaskHandler(db)
|
taskHandler := handlers.NewAgentTaskHandler(db)
|
||||||
agentGroup := api.Group("/agent")
|
agentGroup := api.Group("/agent")
|
||||||
{
|
{
|
||||||
agentGroup.POST("/report", agentHandler.ReceiveReport)
|
agentGroup.POST("/report", agentHandler.ReceiveReport)
|
||||||
agentGroup.GET("/tasks/poll", taskHandler.PollTasks)
|
agentGroup.GET("/tasks/poll", taskHandler.PollTasks)
|
||||||
agentGroup.POST("/tasks/:task_id/result", taskHandler.SubmitResult)
|
agentGroup.POST("/tasks/:task_id/result", taskHandler.SubmitResult)
|
||||||
|
agentGroup.POST("/aggregate", agentHandler.AggregateMetrics)
|
||||||
|
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
||||||
}
|
}
|
||||||
// Agent 数据查询 + 任务管理(需要 JWT)
|
// Agent 数据查询 + 任务管理(需要 JWT)
|
||||||
agentQuery := authorized.Group("/agent")
|
agentQuery := authorized.Group("/agent")
|
||||||
{
|
{
|
||||||
agentQuery.GET("/latest", agentHandler.GetLatestReports)
|
agentQuery.GET("/latest", agentHandler.GetLatestReports)
|
||||||
agentQuery.GET("/history/:hostname", agentHandler.GetHostHistory)
|
agentQuery.GET("/history/:hostname", agentHandler.GetHostHistory)
|
||||||
|
agentQuery.GET("/kline", agentHandler.GetKline)
|
||||||
|
|
||||||
// 远程任务
|
// 远程任务
|
||||||
agentQuery.POST("/tasks", taskHandler.CreateTask)
|
agentQuery.POST("/tasks", taskHandler.CreateTask)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// MetricsKline 指标K线聚合数据(由 Redis 热数据聚合到 MySQL)// Logan.Li
|
||||||
|
type MetricsKline struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Hostname string `gorm:"size:255;index:idx_kline;not null" json:"hostname"`
|
||||||
|
Metric string `gorm:"size:32;index:idx_kline;not null" json:"metric"` // cpu / mem / load1
|
||||||
|
Interval string `gorm:"size:8;index:idx_kline;not null" json:"interval"` // 1m / 3m / 1h / 6h / 1d
|
||||||
|
Open float64 `gorm:"type:decimal(10,2)" json:"open"`
|
||||||
|
High float64 `gorm:"type:decimal(10,2)" json:"high"`
|
||||||
|
Low float64 `gorm:"type:decimal(10,2)" json:"low"`
|
||||||
|
Close float64 `gorm:"type:decimal(10,2)" json:"close"`
|
||||||
|
Avg float64 `gorm:"type:decimal(10,2)" json:"avg"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
BucketTime time.Time `gorm:"index:idx_kline;not null" json:"bucket_time"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MetricsKline) TableName() string {
|
||||||
|
return "metrics_klines"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AggregateKlineReq 聚合请求体
|
||||||
|
type AggregateKlineReq struct {
|
||||||
|
Hostname string `json:"hostname" binding:"required"`
|
||||||
|
Metric string `json:"metric" binding:"required"` // cpu / mem / load1
|
||||||
|
}
|
||||||
|
|
||||||
|
// KlineQueryReq K线查询参数(Query 参数)
|
||||||
|
type KlineQueryReq struct {
|
||||||
|
Hostname string `form:"hostname" binding:"required"`
|
||||||
|
Metric string `form:"metric" binding:"required"`
|
||||||
|
Interval string `form:"interval" binding:"required"`
|
||||||
|
From string `form:"from" binding:"required"` // RFC3339
|
||||||
|
To string `form:"to" binding:"required"` // RFC3339
|
||||||
|
}
|
||||||
@@ -106,6 +106,7 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
&models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
||||||
&models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li
|
&models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li
|
||||||
&models.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li
|
&models.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li
|
||||||
|
&models.MetricsKline{}, // 指标K线聚合(Redis 热数据 → MySQL) // Logan.Li
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|||||||
@@ -64,3 +64,32 @@ func (r *goRedisCache) Ping(ctx context.Context) error {
|
|||||||
func (r *goRedisCache) Close() error {
|
func (r *goRedisCache) Close() error {
|
||||||
return r.client.Close()
|
return r.client.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *goRedisCache) ZAdd(ctx context.Context, key string, member SortedSetMember) error {
|
||||||
|
return r.client.ZAdd(ctx, key, goredis.Z{
|
||||||
|
Score: member.Score,
|
||||||
|
Member: member.Member,
|
||||||
|
}).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goRedisCache) ZRangeByScore(ctx context.Context, key string, min, max string) ([]SortedSetMember, error) {
|
||||||
|
zs, err := r.client.ZRangeByScoreWithScores(ctx, key, &goredis.ZRangeBy{
|
||||||
|
Min: min,
|
||||||
|
Max: max,
|
||||||
|
}).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
members := make([]SortedSetMember, len(zs))
|
||||||
|
for i, z := range zs {
|
||||||
|
members[i] = SortedSetMember{
|
||||||
|
Score: z.Score,
|
||||||
|
Member: z.Member.(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goRedisCache) ZRemRangeByScore(ctx context.Context, key string, min, max string) error {
|
||||||
|
return r.client.ZRemRangeByScore(ctx, key, min, max).Err()
|
||||||
|
}
|
||||||
@@ -34,6 +34,12 @@ type Config struct {
|
|||||||
DataDir string // redka 数据目录
|
DataDir string // redka 数据目录
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortedSetMember 有序集合成员 // Logan.Li
|
||||||
|
type SortedSetMember struct {
|
||||||
|
Score float64
|
||||||
|
Member string
|
||||||
|
}
|
||||||
|
|
||||||
// Cache Redis 缓存接口 // Logan.Li
|
// Cache Redis 缓存接口 // Logan.Li
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
Get(ctx context.Context, key string) (string, error)
|
Get(ctx context.Context, key string) (string, error)
|
||||||
@@ -43,6 +49,11 @@ type Cache interface {
|
|||||||
Expire(ctx context.Context, key string, expiration time.Duration) error
|
Expire(ctx context.Context, key string, expiration time.Duration) error
|
||||||
Ping(ctx context.Context) error
|
Ping(ctx context.Context) error
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
|
// 有序集合操作(热数据指标存储,仅 go-redis 后端完整支持)
|
||||||
|
ZAdd(ctx context.Context, key string, member SortedSetMember) error
|
||||||
|
ZRangeByScore(ctx context.Context, key string, min, max string) ([]SortedSetMember, error)
|
||||||
|
ZRemRangeByScore(ctx context.Context, key string, min, max string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建缓存实例(根据驱动自动选择后端)// Logan.Li
|
// New 创建缓存实例(根据驱动自动选择后端)// Logan.Li
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package redis
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nalgeon/redka"
|
"github.com/nalgeon/redka"
|
||||||
@@ -96,3 +97,17 @@ func (r *redkaCache) Ping(ctx context.Context) error {
|
|||||||
func (r *redkaCache) Close() error {
|
func (r *redkaCache) Close() error {
|
||||||
return r.db.Close()
|
return r.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errSortedSetNotSupported = fmt.Errorf("sorted set operations are not supported by the redka backend; use go-redis (driver=redis) instead")
|
||||||
|
|
||||||
|
func (r *redkaCache) ZAdd(ctx context.Context, key string, member SortedSetMember) error {
|
||||||
|
return errSortedSetNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *redkaCache) ZRangeByScore(ctx context.Context, key string, min, max string) ([]SortedSetMember, error) {
|
||||||
|
return nil, errSortedSetNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *redkaCache) ZRemRangeByScore(ctx context.Context, key string, min, max string) error {
|
||||||
|
return errSortedSetNotSupported
|
||||||
|
}
|
||||||
@@ -491,6 +491,10 @@ export const agentApi = {
|
|||||||
listTasks: (params?: Record<string, string>) => http.get('/agent/tasks', params),
|
listTasks: (params?: Record<string, string>) => http.get('/agent/tasks', params),
|
||||||
// 直连 SSH 执行命令(即时返回)
|
// 直连 SSH 执行命令(即时返回)
|
||||||
execCommand: (data: { hostname: string; command: string; timeout?: number }) => http.post('/agent/exec', data),
|
execCommand: (data: { hostname: string; command: string; timeout?: number }) => http.post('/agent/exec', data),
|
||||||
|
// K线聚合查询
|
||||||
|
getKline: (params: { hostname: string; metric: string; interval: string; from?: string; to?: string }) => http.get('/agent/kline', params),
|
||||||
|
// 触发聚合
|
||||||
|
aggregate: (data: { hostname: string; metric: string }) => http.post('/agent/aggregate', data),
|
||||||
listRules: () => http.get('/agent/rules'),
|
listRules: () => http.get('/agent/rules'),
|
||||||
createRule: (data: any) => http.post('/agent/rules', data),
|
createRule: (data: any) => http.post('/agent/rules', data),
|
||||||
updateRule: (id: number, data: any) => http.put(`/agent/rules/${id}`, data),
|
updateRule: (id: number, data: any) => http.put(`/agent/rules/${id}`, data),
|
||||||
|
|||||||
Reference in new issue
Block a user