fix: 修复Agent指标时区查询
- 统一Agent报告存储与查询的UTC时间边界 - 修复监控大屏、历史图表及离线判断取不到最新数据 - 增加UTC与UTC+8场景回归测试
This commit is contained in:
1 parent
d9106dde6e
commit
7bc416d22a
6 files changed
+83
-9
No files matched your search
@@ -64,6 +64,8 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
reportTime = t
|
reportTime = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Agent 上报时间统一存储为 UTC,避免 SQLite 按带时区文本比较时误判。 // Logan.Li
|
||||||
|
reportTime = reportTime.UTC()
|
||||||
|
|
||||||
matchedServer, matchIP, assetMatched := h.resolveAgentAsset(req.Hostname, req.IP, c.ClientIP())
|
matchedServer, matchIP, assetMatched := h.resolveAgentAsset(req.Hostname, req.IP, c.ClientIP())
|
||||||
canonicalHostname := req.Hostname
|
canonicalHostname := req.Hostname
|
||||||
@@ -260,7 +262,8 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64,
|
|||||||
// GET /api/v1/agent/latest
|
// GET /api/v1/agent/latest
|
||||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||||
var results []model.AgentLatestResponse
|
var results []model.AgentLatestResponse
|
||||||
threeMinAgo := time.Now().Add(-3 * time.Minute)
|
// 查询边界与 Agent 上报的 UTC 时间保持一致。 // Logan.Li
|
||||||
|
threeMinAgo := time.Now().UTC().Add(-3 * time.Minute)
|
||||||
// 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报
|
// 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报
|
||||||
// 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环
|
// 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环
|
||||||
h.db.Raw(`
|
h.db.Raw(`
|
||||||
@@ -394,7 +397,8 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reports []model.AgentReport
|
var reports []model.AgentReport
|
||||||
h.db.Where("hostname = ? AND report_time >= ? AND report_time <= ?", hostname, since, until).
|
// SQLite 会按时间文本比较,查询边界需统一转换为 UTC。 // Logan.Li
|
||||||
|
h.db.Where("hostname = ? AND report_time >= ? AND report_time <= ?", hostname, since.UTC(), until.UTC()).
|
||||||
Order("report_time ASC").
|
Order("report_time ASC").
|
||||||
Find(&reports)
|
Find(&reports)
|
||||||
|
|
||||||
@@ -479,7 +483,8 @@ func (h *AgentHandler) aggregateFromReports(req model.AggregateReq) (int64, erro
|
|||||||
|
|
||||||
for _, iv := range intervals {
|
for _, iv := range intervals {
|
||||||
lookback := iv.duration * 200
|
lookback := iv.duration * 200
|
||||||
since := time.Now().Add(-lookback)
|
// 本地 SQLite 模式下使用 UTC 边界查询 Agent 报告。 // Logan.Li
|
||||||
|
since := time.Now().UTC().Add(-lookback)
|
||||||
var reports []model.AgentReport
|
var reports []model.AgentReport
|
||||||
if err := h.db.Where("hostname = ? AND report_time >= ?", req.Hostname, since).
|
if err := h.db.Where("hostname = ? AND report_time >= ?", req.Hostname, since).
|
||||||
Order("report_time ASC").Find(&reports).Error; err != nil {
|
Order("report_time ASC").Find(&reports).Error; err != nil {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ezops/model"
|
"ezops/model"
|
||||||
|
|
||||||
@@ -76,3 +78,68 @@ func TestReceiveReportMatchesExistingAssetByIP(t *testing.T) {
|
|||||||
t.Fatalf("上报后产生重复资产: count=%d", serverCount)
|
t.Fatalf("上报后产生重复资产: count=%d", serverCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetLatestReportsUsesUTCTimeBoundary(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:agent_latest_utc?mode=memory&cache=shared"), &gorm.Config{
|
||||||
|
Logger: gormlogger.Default.LogMode(gormlogger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("打开测试数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.Server{}, &model.AgentReport{}); err != nil {
|
||||||
|
t.Fatalf("迁移测试表失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec("CREATE TABLE asset_regions (id INTEGER PRIMARY KEY, name TEXT)").Error; err != nil {
|
||||||
|
t.Fatalf("创建区域测试表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := model.Server{
|
||||||
|
Hostname: "c-redis-cluster-3",
|
||||||
|
IP: "172.19.16.59",
|
||||||
|
AgentVersion: "v2.1.0",
|
||||||
|
AgentStatus: "online",
|
||||||
|
DiskUsage: 5.56,
|
||||||
|
}
|
||||||
|
if err := db.Create(&server).Error; err != nil {
|
||||||
|
t.Fatalf("创建测试资产失败: %v", err)
|
||||||
|
}
|
||||||
|
// 模拟 Agent 以 UTC 上报,同时应用运行在 UTC+8 时区。 // Logan.Li
|
||||||
|
report := model.AgentReport{
|
||||||
|
Hostname: server.Hostname,
|
||||||
|
CPUUsage: 12.5,
|
||||||
|
MemUsage: 39.7,
|
||||||
|
CPUCores: 4,
|
||||||
|
ReportTime: time.Now().UTC().Add(-time.Minute),
|
||||||
|
}
|
||||||
|
if err := db.Create(&report).Error; err != nil {
|
||||||
|
t.Fatalf("创建 Agent 报告失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalLocal := time.Local
|
||||||
|
time.Local = time.FixedZone("UTC+8", 8*60*60)
|
||||||
|
defer func() { time.Local = originalLocal }()
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(recorder)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/agent/latest", nil)
|
||||||
|
NewAgentHandler(db, nil, nil).GetLatestReports(c)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("查询返回码 = %d, body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data []model.AgentLatestResponse `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatalf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
if len(response.Data) != 1 {
|
||||||
|
t.Fatalf("主机数量 = %d, body=%s", len(response.Data), recorder.Body.String())
|
||||||
|
}
|
||||||
|
got := response.Data[0]
|
||||||
|
if got.AgentStatus != "online" || got.CPUUsage != 12.5 || got.MemUsage != 39.7 {
|
||||||
|
t.Fatalf("最新指标未命中: status=%q cpu=%.1f mem=%.1f", got.AgentStatus, got.CPUUsage, got.MemUsage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,9 +167,9 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
|||||||
Pluck("hostname", &onlineHosts)
|
Pluck("hostname", &onlineHosts)
|
||||||
|
|
||||||
if len(onlineHosts) == 0 {
|
if len(onlineHosts) == 0 {
|
||||||
// 如果没有在线主机,尝试从 agent_reports 中获取最近 3 分钟活跃的主机名
|
// 如果没有在线主机,使用 UTC 边界从 Agent 报告中查找最近 3 分钟活跃主机。 // Logan.Li
|
||||||
h.db.Model(&model.AgentReport{}).
|
h.db.Model(&model.AgentReport{}).
|
||||||
Where("report_time >= ?", time.Now().Add(-3*time.Minute)).
|
Where("report_time >= ?", time.Now().UTC().Add(-3*time.Minute)).
|
||||||
Distinct("hostname").
|
Distinct("hostname").
|
||||||
Pluck("hostname", &onlineHosts)
|
Pluck("hostname", &onlineHosts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,8 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
|||||||
e.db.Select("ip, hostname").Group("ip").Find(&servers)
|
e.db.Select("ip, hostname").Group("ip").Find(&servers)
|
||||||
|
|
||||||
// 收集所有 agent_reports 中活跃上报的主机名(最近 3 分钟内有上报)
|
// 收集所有 agent_reports 中活跃上报的主机名(最近 3 分钟内有上报)
|
||||||
threeMinAgo := time.Now().Add(-offlineThreshold)
|
// Agent 报告统一使用 UTC,避免 SQLite 时区文本比较导致误报离线。 // Logan.Li
|
||||||
|
threeMinAgo := time.Now().UTC().Add(-offlineThreshold)
|
||||||
|
|
||||||
var activeHostnames []string
|
var activeHostnames []string
|
||||||
e.db.Model(&model.AgentReport{}).
|
e.db.Model(&model.AgentReport{}).
|
||||||
|
|||||||
@@ -366,8 +366,8 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 agent_reports 表查最近 N 小时的真实数据(整数分组,可用索引)
|
// 从 agent_reports 表按 UTC 边界查最近 N 小时的真实数据。 // Logan.Li
|
||||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
since := time.Now().UTC().Add(-time.Duration(hours) * time.Hour)
|
||||||
type avgPoint struct {
|
type avgPoint struct {
|
||||||
BucketTS int64
|
BucketTS int64
|
||||||
AvgCPU float64
|
AvgCPU float64
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ func (s *Service) MetricSummary(ctx context.Context, target, metric, timeRange s
|
|||||||
}
|
}
|
||||||
since := sinceByRange(timeRange, time.Hour)
|
since := sinceByRange(timeRange, time.Hour)
|
||||||
var reports []model.AgentReport
|
var reports []model.AgentReport
|
||||||
if err := s.db.WithContext(ctx).Where("hostname = ? AND report_time >= ?", hostname, since).
|
// Agent 指标时间统一为 UTC,确保 SQLite 按时间文本比较能命中数据。 // Logan.Li
|
||||||
|
if err := s.db.WithContext(ctx).Where("hostname = ? AND report_time >= ?", hostname, since.UTC()).
|
||||||
Order("report_time ASC").Limit(limit).Find(&reports).Error; err != nil {
|
Order("report_time ASC").Limit(limit).Find(&reports).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user