docs: update README, API reference, dev-log, project structure with Agent monitoring + quick-alarm + latest fixes
- README.md: React 19, default pwd 123456, Docker Compose deploy, correct module list - api-reference.md: add Agent monitoring section (report/latest/rules/quick-setup/alerts/tasks/kline) - dev-log.md: new entry documenting hostname/IP fix, system collector, offline detection, quick alarm - project-structure.md: add agent/ directory, dev-log.md link, React 19
This commit is contained in:
1 parent
c51d0b8b8b
commit
4e3f8cb306
20 files changed
+1315
-1344
No files matched your search
@@ -74,7 +74,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 同步更新 servers 表的实时指标(通过 hostname 匹配)
|
||||
// 同步更新 servers 表的实时指标(三级匹配:hostname → client IP → hostname LIKE)
|
||||
diskAvg := avgDiskUsage(req.Disks)
|
||||
|
||||
// 计算硬件规格
|
||||
@@ -84,19 +84,48 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
}
|
||||
memMB := int64(req.Memory.TotalGB * 1024) // GB → MB
|
||||
|
||||
result := h.db.Model(&models.Server{}).
|
||||
Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).
|
||||
Updates(map[string]interface{}{
|
||||
"cpu_usage": req.CPU.UsagePercent,
|
||||
"memory_usage": req.Memory.UsagePercent,
|
||||
"disk_usage": diskAvg,
|
||||
"cpu": req.CPU.CoreCount,
|
||||
"memory": memMB,
|
||||
"disk": int64(totalDiskGB),
|
||||
"status": "running",
|
||||
"agent_status": "online",
|
||||
"agent_version": req.AgentVersion,
|
||||
})
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 三级匹配:优先用 Agent 上报的 IP,其次用请求源 IP,最后用报文的 hostname
|
||||
matchIP := req.IP
|
||||
if matchIP == "" {
|
||||
matchIP = clientIP
|
||||
}
|
||||
|
||||
// 直接 SQL 更新,避免 GORM soft delete 自动添加 deleted_at 条件干扰
|
||||
updateSQL := `UPDATE servers SET
|
||||
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
|
||||
cpu = ?, memory = ?, disk = ?,
|
||||
status = ?, agent_status = ?, agent_version = ?,
|
||||
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
||||
updated_at = NOW()`
|
||||
args := []interface{}{
|
||||
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
|
||||
req.CPU.CoreCount, memMB, int64(totalDiskGB),
|
||||
"running", "online", req.AgentVersion,
|
||||
// CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END
|
||||
req.Hostname, matchIP, req.Hostname,
|
||||
}
|
||||
if req.System != nil && req.System.OSVersion != "" {
|
||||
updateSQL += ", os = ?"
|
||||
args = append(args, req.System.OSVersion)
|
||||
}
|
||||
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 上报了,但服务器列表里没有对应条目)
|
||||
if result.RowsAffected == 0 {
|
||||
fmt.Printf("[agent] 上报主机 %q (IP: %s) 未匹配任何 servers 条目,指标仅写入 agent_reports 表\n", req.Hostname, matchIP)
|
||||
}
|
||||
|
||||
// WebSocket 广播:向所有前端客户端推送该主机的最新指标 // Logan.Li
|
||||
if h.hubMetrics != nil && result.RowsAffected > 0 {
|
||||
@@ -154,10 +183,11 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64,
|
||||
_ = h.cache.ZRemRangeByScore(ctx, key, "-inf", fmt.Sprintf("%.0f", cutoff))
|
||||
}
|
||||
|
||||
// GetLatestReports 获取各主机最新上报
|
||||
// GetLatestReports 获取各主机最新上报(仅 3 分钟内活跃的 hostname)
|
||||
// GET /api/v1/agent/latest
|
||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
var results []models.AgentLatestResponse
|
||||
threeMinAgo := time.Now().Add(-3 * time.Minute)
|
||||
h.db.Raw(`
|
||||
SELECT
|
||||
a.hostname,
|
||||
@@ -179,10 +209,13 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
LEFT JOIN servers s ON s.hostname = a.hostname
|
||||
LEFT JOIN regions r ON r.id = s.region_id
|
||||
WHERE a.id IN (
|
||||
SELECT MAX(id) FROM agent_reports GROUP BY hostname
|
||||
SELECT MAX(id) FROM agent_reports
|
||||
WHERE report_time >= ?
|
||||
GROUP BY hostname
|
||||
)
|
||||
AND s.id IS NOT NULL -- 只显示匹配 servers 表的主机(首次上线后自动同步 hostname)
|
||||
ORDER BY a.report_time DESC
|
||||
`).Scan(&results)
|
||||
`, threeMinAgo).Scan(&results)
|
||||
|
||||
if results == nil {
|
||||
results = []models.AgentLatestResponse{}
|
||||
|
||||
@@ -152,6 +152,82 @@ func (h *AgentTaskHandler) GetTask(c *gin.Context) {
|
||||
|
||||
// ===== Agent 告警规则 =====
|
||||
|
||||
// QuickSetupRules POST /api/v1/agent/rules/quick-setup
|
||||
// 一键创建默认告警规则(CPU/内存/磁盘/网络带宽),作用于所有在线主机
|
||||
func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type"` // "ecs" 或留空
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
// 获取所有在线主机
|
||||
var onlineHosts []string
|
||||
h.db.Model(&models.Server{}).
|
||||
Where("agent_status = ?", "online").
|
||||
Pluck("hostname", &onlineHosts)
|
||||
|
||||
if len(onlineHosts) == 0 {
|
||||
// 如果没有在线主机,尝试从 agent_reports 中获取最近 3 分钟活跃的主机名
|
||||
h.db.Model(&models.AgentReport{}).
|
||||
Where("report_time >= ?", time.Now().Add(-3*time.Minute)).
|
||||
Distinct("hostname").
|
||||
Pluck("hostname", &onlineHosts)
|
||||
}
|
||||
|
||||
if len(onlineHosts) == 0 {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "没有在线的主机,请先安装并启动 Agent"})
|
||||
return
|
||||
}
|
||||
|
||||
type ruleDef struct {
|
||||
Name string
|
||||
Metric string
|
||||
Operator string
|
||||
Threshold float64
|
||||
Duration int
|
||||
}
|
||||
|
||||
defaultRules := []ruleDef{
|
||||
{Name: "CPU使用率(Warn)", Metric: "cpu", Operator: ">", Threshold: 80},
|
||||
{Name: "内存使用率(Warn)", Metric: "mem", Operator: ">", Threshold: 80},
|
||||
{Name: "磁盘使用率(Warn)", Metric: "disk", Operator: ">", Threshold: 80},
|
||||
{Name: "网络带宽使用率(Warn)", Metric: "net", Operator: ">", Threshold: 80},
|
||||
}
|
||||
|
||||
created := 0
|
||||
skipped := 0
|
||||
for _, host := range onlineHosts {
|
||||
for _, def := range defaultRules {
|
||||
// 检查是否已存在同名同主机的规则,避免重复创建
|
||||
var existing models.AgentRule
|
||||
h.db.Where("name = ? AND hostname = ?", def.Name, host).First(&existing)
|
||||
if existing.ID > 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
rule := models.AgentRule{
|
||||
Name: def.Name,
|
||||
Hostname: host,
|
||||
Metric: def.Metric,
|
||||
Operator: def.Operator,
|
||||
Threshold: def.Threshold,
|
||||
Enabled: true,
|
||||
}
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已为 %d 台主机创建 %d 条规则(跳过 %d 条已存在的)", len(onlineHosts), created, skipped),
|
||||
Data: gin.H{"hosts": len(onlineHosts), "created": created, "skipped": skipped},
|
||||
})
|
||||
}
|
||||
|
||||
// ListRules GET /api/v1/agent/rules
|
||||
func (h *AgentTaskHandler) ListRules(c *gin.Context) {
|
||||
var rules []models.AgentRule
|
||||
|
||||
@@ -95,36 +95,74 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查主机离线(超过 3 分钟无上报)— 内置规则,无需手动创建
|
||||
// 检查主机离线(超过 3 分钟无上报)— 内置规则,无需手动创建。
|
||||
// 以 servers 表为中心:遍历所有服务器,检查其 IP 上是否有 Agent 在最近 3 分钟内上报。
|
||||
// 如果同一 IP 下有多个 Agent hostname,只要有一个在活跃上报,该服务器就算在线。
|
||||
func (e *AlertEngine) checkOfflineAlerts() {
|
||||
offlineThreshold := 3 * time.Minute
|
||||
|
||||
// 查所有曾上报的主机
|
||||
var hostnames []string
|
||||
e.db.Model(&models.AgentReport{}).Distinct("hostname").Pluck("hostname", &hostnames)
|
||||
// 获取所有已登记的主机(去重 IP)
|
||||
var servers []models.Server
|
||||
e.db.Select("ip, hostname").Group("ip").Find(&servers)
|
||||
|
||||
for _, hostname := range hostnames {
|
||||
var report models.AgentReport
|
||||
e.db.Where("hostname = ?", hostname).Order("id DESC").First(&report)
|
||||
if report.ID == 0 {
|
||||
// 收集所有 agent_reports 中活跃上报的主机名(最近 3 分钟内有上报)
|
||||
threeMinAgo := time.Now().Add(-offlineThreshold)
|
||||
|
||||
var activeHostnames []string
|
||||
e.db.Model(&models.AgentReport{}).
|
||||
Where("report_time >= ?", threeMinAgo).
|
||||
Distinct("hostname").
|
||||
Pluck("hostname", &activeHostnames)
|
||||
|
||||
// 对于每个已登记的服务器,检查是否有活跃 Agent
|
||||
for _, s := range servers {
|
||||
if s.IP == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
isOffline := time.Since(report.ReportTime) > offlineThreshold
|
||||
// 检查该 IP 对应的服务器是否有活跃 Agent 上报
|
||||
hasActiveAgent := false
|
||||
for _, h := range activeHostnames {
|
||||
// 通过 hostname 精确匹配或 servers 表 IP 匹配
|
||||
if h == s.Hostname {
|
||||
hasActiveAgent = true
|
||||
break
|
||||
}
|
||||
// 检查该 hostname 的最新上报——其源 IP 是否匹配此 server IP
|
||||
// (通过 servers 表中查找 hostname 对应的 IP)
|
||||
var matchedServer models.Server
|
||||
e.db.Where("hostname = ?", h).First(&matchedServer)
|
||||
if matchedServer.ID > 0 && matchedServer.IP == s.IP {
|
||||
hasActiveAgent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有未恢复的离线告警
|
||||
if hasActiveAgent {
|
||||
continue // 有活跃 Agent,在线
|
||||
}
|
||||
|
||||
// 该服务器离线:检查是否已有未恢复的离线告警
|
||||
var existing models.AgentAlert
|
||||
e.db.Where("hostname = ? AND metric = ? AND resolved = ?",
|
||||
hostname, "offline", false).First(&existing)
|
||||
s.Hostname, "offline", false).First(&existing)
|
||||
|
||||
if existing.ID == 0 {
|
||||
// 获取最后一次上报时间计算离线时长
|
||||
var lastReport models.AgentReport
|
||||
e.db.Order("id DESC").First(&lastReport)
|
||||
offlineMinutes := 3.0
|
||||
if lastReport.ID > 0 {
|
||||
offlineMinutes = time.Since(lastReport.ReportTime).Minutes()
|
||||
}
|
||||
|
||||
if isOffline && existing.ID == 0 {
|
||||
alert := models.AgentAlert{
|
||||
RuleName: "主机离线",
|
||||
Hostname: hostname,
|
||||
Hostname: s.Hostname,
|
||||
Metric: "offline",
|
||||
Severity: "critical",
|
||||
Message: fmt.Sprintf("主机 %s 已超过 3 分钟无数据上报,可能已离线", hostname),
|
||||
Value: time.Since(report.ReportTime).Minutes(),
|
||||
Message: fmt.Sprintf("主机 %s (%s) 已超过 3 分钟无数据上报,可能已离线", s.Hostname, s.IP),
|
||||
Value: offlineMinutes,
|
||||
Threshold: 3,
|
||||
}
|
||||
e.db.Create(&alert)
|
||||
|
||||
@@ -443,6 +443,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
|
||||
// 告警规则
|
||||
agentQuery.GET("/rules", taskHandler.ListRules)
|
||||
agentQuery.POST("/rules/quick-setup", taskHandler.QuickSetupRules)
|
||||
agentQuery.POST("/rules", taskHandler.CreateRule)
|
||||
agentQuery.PUT("/rules/:id", taskHandler.UpdateRule)
|
||||
agentQuery.DELETE("/rules/:id", taskHandler.DeleteRule)
|
||||
|
||||
Reference in new issue
Block a user