fix: 修复Agent按IP关联资产

- 统一系统主机名与云资产名称的指标归属
- 修复SQLite更新参数顺序和自动注册冲突错误
- 增加Agent上报关联回归测试
This commit is contained in:
Logan.Li committed 2026-07-25 15:46:08 +08:00
1 parent 827d9a31cd
commit af98940211
2 files changed
+121 -42

No files matched your search

+43 -42
View File
@@ -30,6 +30,24 @@ func NewAgentHandler(db *gorm.DB, cache ezredis.Cache, hub *MetricsHub) *AgentHa
return &AgentHandler{db: db, cache: cache, hubMetrics: hub}
}
// resolveAgentAsset 优先按上报主机名或内网 IP 关联现有资产,避免云实例名与系统 hostname 不一致。 // Logan.Li
func (h *AgentHandler) resolveAgentAsset(hostname, reportIP, clientIP string) (model.Server, string, bool) {
matchIP := strings.TrimSpace(reportIP)
if matchIP == "" || matchIP == "unknown" {
matchIP = strings.TrimSpace(clientIP)
}
var server model.Server
query := h.db.Where("hostname = ?", hostname)
if matchIP != "" {
query = h.db.Where("hostname = ? OR ip = ?", hostname, matchIP)
}
if err := query.Take(&server).Error; err == nil {
return server, matchIP, true
}
return server, matchIP, false
}
// ReceiveReport 接收 Agent 上报的指标数据
// POST /api/v1/agent/report
func (h *AgentHandler) ReceiveReport(c *gin.Context) {
@@ -47,6 +65,12 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
}
}
matchedServer, matchIP, assetMatched := h.resolveAgentAsset(req.Hostname, req.IP, c.ClientIP())
canonicalHostname := req.Hostname
if assetMatched {
canonicalHostname = matchedServer.Hostname
}
// 序列化磁盘和端口信息
diskJSON, _ := json.Marshal(req.Disks)
portJSON, _ := json.Marshal(req.Ports)
@@ -61,7 +85,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
// 存储到数据库
report := model.AgentReport{
Hostname: req.Hostname,
Hostname: canonicalHostname,
CPUUsage: req.CPU.UsagePercent,
CPUIOwait: req.CPU.IOWait,
CPUCores: req.CPU.CoreCount,
@@ -103,14 +127,6 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
memMB := int64(memGB * 1024) // GB → MB
diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40
clientIP := c.ClientIP()
// 三级匹配:优先用 Agent 上报的 IP,其次用请求源 IP,最后用报文的 hostname
matchIP := req.IP
if matchIP == "" {
matchIP = clientIP
}
// 直接 SQL 更新 asset_servers 监控字段
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
updateSQL := `UPDATE asset_servers SET
@@ -121,9 +137,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
return `, agent_version = ?`
}
return ``
}() + `,
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
updated_at = ?`
}()
args := []interface{}{
req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg,
req.CPU.CoreCount, memMB, diskGB,
@@ -132,10 +146,6 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
if req.AgentVersion != "" {
args = append(args, req.AgentVersion)
}
args = append(args,
// CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END
req.Hostname, matchIP, req.Hostname,
)
if req.System != nil {
if req.System.OSVersion != "" {
updateSQL += ", os = ?"
@@ -150,29 +160,28 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
args = append(args, req.System.Uptime)
}
}
updateSQL += ", updated_at = ?"
args = append(args, time.Now())
updateSQL += " WHERE hostname = ? OR ip = ?"
args = append(args, req.Hostname, matchIP)
if assetMatched {
updateSQL += " WHERE id = ?"
args = append(args, matchedServer.ID)
} else {
updateSQL += " WHERE hostname = ? OR ip = ?"
args = append(args, req.Hostname, matchIP)
}
result := h.db.Exec(updateSQL, args...)
// 如果 hostname 和 IP 都没匹配到,尝试用 hostname 反向匹配 IP 字段
if result.RowsAffected == 0 && req.Hostname != matchIP {
updateSQL2 := updateSQL + " OR ip = ?"
args2 := append(args, req.Hostname)
result = h.db.Exec(updateSQL2, args2...)
}
// 如果还是没匹配到 —— 按 hostname 显式查重后再注册 // Logan.Li
// asset_servers.hostname 无 UNIQUE 索引,OnConflict 不可靠,故先 First 查重,
// 避免 Agent 每次上报(10s)都新建记录导致资产越来越多。
if result.RowsAffected == 0 {
if result.Error == nil && result.RowsAffected == 0 {
osName := ""
if req.System != nil {
osName = req.System.OSVersion
}
var existing model.Server
if h.db.Where("hostname = ?", req.Hostname).First(&existing).Error == nil {
if h.db.Where("hostname = ?", req.Hostname).Take(&existing).Error == nil {
// 已自动注册过 → 仅更新实时指标,不重复创建
h.db.Model(&existing).Updates(map[string]interface{}{
"ip": matchIP, "os": osName, "cpu": req.CPU.CoreCount,
@@ -182,7 +191,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
"disk_usage": diskAvg,
})
} else {
// 真正的新主机 → 注册(OnConflict 作为并发双保险)
// 真正的新主机 → 注册,hostname 无唯一索引时不使用 OnConflict。 // Logan.Li
newServer := model.Server{
Hostname: req.Hostname,
IP: matchIP,
@@ -200,15 +209,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
MemoryUsage: req.Memory.UsagePercent,
DiskUsage: diskAvg,
}
if err := h.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "hostname"}},
DoUpdates: clause.AssignmentColumns([]string{
"ip", "os", "cpu", "memory", "disk",
"status", "agent_status",
"cpu_usage", "memory_usage", "disk_usage",
"updated_at",
}),
}).Create(&newServer).Error; err != nil {
if err := h.db.Create(&newServer).Error; err != nil {
fmt.Printf("[agent] 自动注册主机 %q (IP: %s) 失败: %v\n", req.Hostname, matchIP, err)
} else {
fmt.Printf("[agent] 自动注册新主机: %q (IP: %s, CPU=%d核, 内存=%dMB, 磁盘=%dGB, OS=%q)\n",
@@ -221,7 +222,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
if h.hubMetrics != nil && result.RowsAffected > 0 {
h.hubMetrics.Broadcast(MetricsUpdate{
Type: "metrics_update",
Hostname: req.Hostname,
Hostname: canonicalHostname,
CPU: req.CPU.UsagePercent,
Memory: req.Memory.UsagePercent,
Disk: diskAvg,
@@ -239,13 +240,13 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
if h.cache != nil && h.cache.Capabilities().SortedSet {
timestamp := float64(reportTime.Unix())
ttl := 7 * 24 * time.Hour
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)
h.writeHotMetric(c, "metrics:cpu:"+canonicalHostname, req.CPU.UsagePercent, timestamp, ttl, reportTime)
h.writeHotMetric(c, "metrics:mem:"+canonicalHostname, req.Memory.UsagePercent, timestamp, ttl, reportTime)
h.writeHotMetric(c, "metrics:load1:"+canonicalHostname, req.CPU.Load1, timestamp, ttl, reportTime)
}
// 保存成功 — 响应中携带探针配置给 Agent
resp := h.makeProbeConfigResponse(req.Hostname, 200, "ok", req.ProbeResults)
resp := h.makeProbeConfigResponse(canonicalHostname, 200, "ok", req.ProbeResults)
c.JSON(http.StatusOK, resp)
}