chore: 统一本地服务器与远程仓库源码
- 合并 main 与 master 历史并以服务器结构为基线 - 保留迁移、数据保留、AWS 账单与部署安全能力 - 建立 SDD 规格和 CI 验证闭环
This commit is contained in:
commit
fd60361d93
460 files changed
+37492
-23871
No files matched your search
+98
-92
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/middleware"
|
||||
"ezops/model"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
@@ -30,6 +31,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) {
|
||||
@@ -38,6 +57,9 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if !middleware.RequireAgentHostname(c, req.Hostname) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析上报时间
|
||||
reportTime := time.Now()
|
||||
@@ -46,6 +68,14 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
reportTime = t
|
||||
}
|
||||
}
|
||||
// Agent 上报时间统一存储为 UTC,避免 SQLite 按带时区文本比较时误判。 // Logan.Li
|
||||
reportTime = reportTime.UTC()
|
||||
|
||||
matchedServer, matchIP, assetMatched := h.resolveAgentAsset(req.Hostname, req.IP, c.ClientIP())
|
||||
canonicalHostname := req.Hostname
|
||||
if assetMatched {
|
||||
canonicalHostname = matchedServer.Hostname
|
||||
}
|
||||
|
||||
// 序列化磁盘和端口信息
|
||||
diskJSON, _ := json.Marshal(req.Disks)
|
||||
@@ -61,8 +91,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
|
||||
// 存储到数据库
|
||||
report := model.AgentReport{
|
||||
Hostname: req.Hostname,
|
||||
IP: strings.TrimSpace(req.IP), // 使用 Agent 自报 IP,避免反向代理地址误关联。 // Logan.Li
|
||||
Hostname: canonicalHostname,
|
||||
CPUUsage: req.CPU.UsagePercent,
|
||||
CPUIOwait: req.CPU.IOWait,
|
||||
CPUCores: req.CPU.CoreCount,
|
||||
@@ -104,86 +133,71 @@ 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
|
||||
cpu_usage = ?, cpu_iowait = ?, memory_usage = ?, disk_usage = ?,
|
||||
cpu = ?, memory = ?, disk = ?,
|
||||
status = ?, agent_status = ?` + func() string {
|
||||
if req.AgentVersion != "" {
|
||||
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,
|
||||
"running", "online",
|
||||
// 使用 map 按主键更新,避免可选系统字段导致原始 SQL 占位参数错位。 // Logan.Li
|
||||
updates := map[string]interface{}{
|
||||
"cpu_usage": req.CPU.UsagePercent, "memory_usage": req.Memory.UsagePercent,
|
||||
"disk_usage": diskAvg,
|
||||
"cpu": req.CPU.CoreCount, "memory": memMB, "disk": diskGB,
|
||||
"status": "running", "agent_status": "online", "updated_at": time.Now(),
|
||||
}
|
||||
if req.AgentVersion != "" {
|
||||
args = append(args, req.AgentVersion)
|
||||
updates["agent_version"] = 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 = ?"
|
||||
args = append(args, req.System.OSVersion)
|
||||
updates["os"] = req.System.OSVersion
|
||||
}
|
||||
if req.System.Kernel != "" {
|
||||
updateSQL += ", kernel = ?"
|
||||
args = append(args, req.System.Kernel)
|
||||
updates["kernel"] = req.System.Kernel
|
||||
}
|
||||
if req.System.Uptime > 0 {
|
||||
updateSQL += ", uptime = ?"
|
||||
args = append(args, req.System.Uptime)
|
||||
updates["uptime"] = req.System.Uptime
|
||||
}
|
||||
}
|
||||
args = append(args, time.Now())
|
||||
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...)
|
||||
// 兼容旧版 Agent:系统信息缺席时使用心跳中的运行时长补齐资产元数据。 // Logan.Li
|
||||
if _, ok := updates["uptime"]; !ok && req.Heartbeat.UptimeSec > 0 {
|
||||
updates["uptime"] = req.Heartbeat.UptimeSec
|
||||
}
|
||||
var result *gorm.DB
|
||||
if assetMatched {
|
||||
result = h.db.Model(&model.Server{}).Where("id = ?", matchedServer.ID).Updates(updates)
|
||||
} else {
|
||||
result = h.db.Model(&model.Server{}).Where("hostname = ? OR ip = ?", req.Hostname, matchIP).Updates(updates)
|
||||
}
|
||||
|
||||
// 如果还是没匹配到 —— 按 hostname 显式查重后再注册 // Logan.Li
|
||||
// asset_servers.hostname 无 UNIQUE 索引,OnConflict 不可靠,故先 First 查重,
|
||||
// 避免 Agent 每次上报(10s)都新建记录导致资产越来越多。
|
||||
if result.RowsAffected == 0 {
|
||||
if result.Error == nil && result.RowsAffected == 0 {
|
||||
osName := ""
|
||||
kernel := ""
|
||||
uptime := req.Heartbeat.UptimeSec
|
||||
if req.System != nil {
|
||||
osName = req.System.OSVersion
|
||||
kernel = req.System.Kernel
|
||||
if req.System.Uptime > 0 {
|
||||
uptime = req.System.Uptime
|
||||
}
|
||||
}
|
||||
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{}{
|
||||
existingUpdates := map[string]interface{}{
|
||||
"ip": matchIP, "os": osName, "cpu": req.CPU.CoreCount,
|
||||
"memory": memMB, "disk": diskGB, "status": "running",
|
||||
"agent_status": "online", "agent_version": req.AgentVersion,
|
||||
"cpu_usage": req.CPU.UsagePercent, "memory_usage": req.Memory.UsagePercent,
|
||||
"disk_usage": diskAvg,
|
||||
})
|
||||
}
|
||||
if kernel != "" {
|
||||
existingUpdates["kernel"] = kernel
|
||||
}
|
||||
if uptime > 0 {
|
||||
existingUpdates["uptime"] = uptime
|
||||
}
|
||||
h.db.Model(&existing).Updates(existingUpdates)
|
||||
} else {
|
||||
// 真正的新主机 → 注册(OnConflict 作为并发双保险)
|
||||
// 真正的新主机 → 注册,hostname 无唯一索引时不使用 OnConflict。 // Logan.Li
|
||||
newServer := model.Server{
|
||||
Hostname: req.Hostname,
|
||||
IP: matchIP,
|
||||
@@ -197,19 +211,13 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
Status: "running",
|
||||
AgentStatus: "online",
|
||||
AgentVersion: req.AgentVersion,
|
||||
Kernel: kernel,
|
||||
Uptime: uptime,
|
||||
CPUUsage: req.CPU.UsagePercent,
|
||||
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",
|
||||
@@ -222,7 +230,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,
|
||||
@@ -240,13 +248,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)
|
||||
}
|
||||
|
||||
@@ -277,8 +285,9 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64,
|
||||
// GET /api/v1/agent/latest
|
||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
var results []model.AgentLatestResponse
|
||||
threeMinAgo := time.Now().UTC().Add(-3 * time.Minute) // SQLite 上报时间统一以 UTC 持久化。 // Logan.Li
|
||||
// 以 servers 表为主,按资产名或 Agent 自报 IP 关联最近 3 分钟内的上报。 // Logan.Li
|
||||
// 查询边界与 Agent 上报的 UTC 时间保持一致。 // Logan.Li
|
||||
threeMinAgo := time.Now().UTC().Add(-3 * time.Minute)
|
||||
// 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报
|
||||
// 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环
|
||||
h.db.Raw(`
|
||||
SELECT
|
||||
@@ -303,8 +312,7 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
a.report_time,
|
||||
COALESCE(s.agent_version, '') as agent_version,
|
||||
CASE
|
||||
WHEN a.id IS NOT NULL THEN 'online'
|
||||
WHEN COALESCE(s.agent_version, '') <> '' OR COALESCE(s.agent_status, '') = 'online' THEN 'offline'
|
||||
WHEN a.hostname IS NOT NULL THEN COALESCE(s.agent_status, 'online')
|
||||
ELSE 'not_installed'
|
||||
END as agent_status,
|
||||
COALESCE(s.os, '') as os,
|
||||
@@ -314,13 +322,14 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
COALESCE(r.name, '') as region,
|
||||
CASE WHEN s.password != '' OR s.ssh_key != '' THEN true ELSE false END as has_auth
|
||||
FROM asset_servers s
|
||||
LEFT JOIN monitor_agent_reports a ON a.id = (
|
||||
SELECT ar.id FROM monitor_agent_reports ar
|
||||
WHERE ar.report_time >= ?
|
||||
AND (ar.hostname = s.hostname OR (ar.ip <> '' AND ar.ip = s.ip))
|
||||
ORDER BY ar.report_time DESC, ar.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT * FROM monitor_agent_reports
|
||||
WHERE id IN (
|
||||
SELECT MAX(id) FROM monitor_agent_reports
|
||||
WHERE report_time >= ?
|
||||
GROUP BY hostname
|
||||
)
|
||||
) a ON a.hostname = s.hostname
|
||||
LEFT JOIN asset_regions r ON r.id = s.region_id
|
||||
ORDER BY a.report_time DESC, s.id ASC
|
||||
`, threeMinAgo).Scan(&results)
|
||||
@@ -348,7 +357,7 @@ func parseFlexibleTime(s string) (time.Time, error) {
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
|
||||
return t.UTC(), nil // 历史报告使用 UTC 存储,避免自定义时间范围跨时区漏数。 // Logan.Li
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("无法解析时间: %s", s)
|
||||
@@ -356,7 +365,7 @@ func parseFlexibleTime(s string) (time.Time, error) {
|
||||
|
||||
// resolveHostHistoryRange 解析历史查询时间范围(minutes / hours / start+end) // Logan.Li
|
||||
func resolveHostHistoryRange(c *gin.Context) (since, until time.Time, err error) {
|
||||
until = time.Now().UTC() // 历史报告使用 UTC 存储,避免本地时区造成范围比较失效。 // Logan.Li
|
||||
until = time.Now()
|
||||
if startStr := strings.TrimSpace(c.Query("start")); startStr != "" {
|
||||
endStr := strings.TrimSpace(c.Query("end"))
|
||||
if endStr == "" {
|
||||
@@ -410,16 +419,9 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var server model.Server
|
||||
serverIP := ""
|
||||
if h.db.Where("hostname = ?", hostname).Take(&server).Error == nil {
|
||||
serverIP = strings.TrimSpace(server.IP)
|
||||
}
|
||||
|
||||
var reports []model.AgentReport
|
||||
// 资产名可能与操作系统主机名不同,优先兼容 Agent 自报 IP。 // Logan.Li
|
||||
h.db.Where("hostname = ? OR (ip <> '' AND ip = ?)", hostname, serverIP).
|
||||
Where("report_time >= ? AND report_time <= ?", 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").
|
||||
Find(&reports)
|
||||
|
||||
@@ -431,7 +433,7 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
||||
}
|
||||
|
||||
// AggregateMetrics 聚合指标到 monitor_metrics_aggregate(Redis ZSet 或 DB 直读)// Logan.Li
|
||||
// POST /api/v1/agent/aggregate
|
||||
// POST /api/v1/agent/aggregate(需要控制台 JWT) // Logan.Li
|
||||
func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||
var req model.AggregateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -504,7 +506,8 @@ func (h *AgentHandler) aggregateFromReports(req model.AggregateReq) (int64, erro
|
||||
|
||||
for _, iv := range intervals {
|
||||
lookback := iv.duration * 200
|
||||
since := time.Now().Add(-lookback)
|
||||
// 本地 SQLite 模式下使用 UTC 边界查询 Agent 报告。 // Logan.Li
|
||||
since := time.Now().UTC().Add(-lookback)
|
||||
var reports []model.AgentReport
|
||||
if err := h.db.Where("hostname = ? AND report_time >= ?", req.Hostname, since).
|
||||
Order("report_time ASC").Find(&reports).Error; err != nil {
|
||||
@@ -695,6 +698,9 @@ func avgDiskUsage(disks []model.DiskMetric) float64 {
|
||||
// GET /api/v1/agent/port-policy?hostname=xxx
|
||||
func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if !middleware.RequireAgentHostname(c, hostname) {
|
||||
return
|
||||
}
|
||||
if hostname == "" {
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
PortFilterMode: "whitelist",
|
||||
|
||||
Reference in new issue
Block a user