package controller import ( "net/http" "strconv" "time" "ezops/middleware" "ezops/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ──────────────────────────────────────── // HealthProbe CRUD(健康探针配置管理) // ──────────────────────────────────────── // ListHealthProbes GET /api/v1/agent/health-probes func (h *AgentHandler) ListHealthProbes(c *gin.Context) { query := h.db.Model(&model.HealthProbe{}) if hostname := c.Query("hostname"); hostname != "" { query = query.Where("hostname = ?", hostname) } if enabled := c.Query("enabled"); enabled != "" { if enabled == "true" { query = query.Where("enabled = ?", true) } else if enabled == "false" { query = query.Where("enabled = ?", false) } } var probes []model.HealthProbe query.Order("created_at DESC").Limit(500).Find(&probes) if probes == nil { probes = []model.HealthProbe{} } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: probes, }) } // CreateHealthProbe POST /api/v1/agent/health-probes func (h *AgentHandler) CreateHealthProbe(c *gin.Context) { var probe model.HealthProbe if err := c.ShouldBindJSON(&probe); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()}) return } if probe.Name == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针名称不能为空"}) return } if probe.Hostname == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "目标主机不能为空"}) return } if probe.ProbeType == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针类型不能为空"}) return } // 验证探针类型配置 switch probe.ProbeType { case model.ProbeTypeHTTP: if probe.HTTPURL == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "HTTP 探针必须填写 URL"}) return } case model.ProbeTypeTCP: if probe.TCPHost == "" || probe.TCPPort == 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "TCP 探针必须填写 Host 和 Port"}) return } case model.ProbeTypeExec: if probe.ExecCommand == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "Exec 探针必须填写执行命令"}) return } default: c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "不支持的探针类型: " + string(probe.ProbeType)}) return } probe.ID = 0 probe.Enabled = true if probe.Timeout == 0 { probe.Timeout = 10 } if probe.Interval == 0 { probe.Interval = 60 } if err := h.db.Create(&probe).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: probe}) } // UpdateHealthProbe PUT /api/v1/agent/health-probes/:id func (h *AgentHandler) UpdateHealthProbe(c *gin.Context) { id := c.Param("id") var probe model.HealthProbe if err := h.db.First(&probe, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "探针不存在"}) return } var input model.HealthProbe if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) return } updates := map[string]interface{}{} if input.Name != "" { updates["name"] = input.Name } if input.Hostname != "" { updates["hostname"] = input.Hostname } if input.ProbeType != "" { updates["probe_type"] = input.ProbeType } if input.HTTPURL != "" { updates["http_url"] = input.HTTPURL } if input.TCPHost != "" { updates["tcp_host"] = input.TCPHost } if input.TCPPort != 0 { updates["tcp_port"] = input.TCPPort } if input.ExecCommand != "" { updates["exec_command"] = input.ExecCommand } if input.Timeout > 0 { updates["timeout"] = input.Timeout } if input.Interval > 0 { updates["interval"] = input.Interval } // enabled 布尔处理:显式传入 updates["enabled"] = input.Enabled if input.HTTPMethod != "" { updates["http_method"] = input.HTTPMethod } if input.HTTPHeaders != "" { updates["http_headers"] = input.HTTPHeaders } if input.HTTPExpectCode > 0 { updates["http_expect_code"] = input.HTTPExpectCode } if input.HTTPExpectBody != "" { updates["http_expect_body"] = input.HTTPExpectBody } if input.Description != "" { updates["description"] = input.Description } if err := h.db.Model(&probe).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()}) return } h.db.First(&probe, id) c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: probe}) } // DeleteHealthProbe DELETE /api/v1/agent/health-probes/:id func (h *AgentHandler) DeleteHealthProbe(c *gin.Context) { id := c.Param("id") if err := h.db.Delete(&model.HealthProbe{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // GetHealthProbesForAgent Agent 拉取本机探针配置(Agent 调用,无需 JWT) // GET /api/v1/agent/health-probes/config?hostname=xxx func (h *AgentHandler) GetHealthProbesForAgent(c *gin.Context) { hostname := c.Query("hostname") if hostname == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "hostname 不能为空"}) return } if !middleware.RequireAgentHostname(c, hostname) { return } var probes []model.HealthProbe h.db.Where("hostname = ? AND enabled = ?", hostname, true). Order("id ASC"). Find(&probes) if probes == nil { probes = []model.HealthProbe{} } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: probes, }) } // ──────────────────────────────────────── // ReceiveReport 增强:处理探针结果 + 返回配置 // ──────────────────────────────────────── // processProbeResults 处理 Agent 上报的探针结果,更新探针状态 func (h *AgentHandler) processProbeResults(hostname string, results []model.HealthProbeResult) { now := time.Now() for _, r := range results { updates := map[string]interface{}{ "last_status": string(r.Status), "last_checked_at": now, "last_latency_ms": r.LatencyMs, } if r.Status == model.ProbeStatusDown { // 失败时增加连续失败计数 h.db.Model(&model.HealthProbe{}). Where("id = ? AND hostname = ?", r.ProbeID, hostname). UpdateColumn("fail_count", gorm.Expr("fail_count + 1")) } else { // 成功时清零 updates["fail_count"] = 0 } h.db.Model(&model.HealthProbe{}). Where("id = ? AND hostname = ?", r.ProbeID, hostname). Updates(updates) // ═══ 探针告警:Agent 本地探测 down → 平台创建告警事件 ═══ if r.Status == model.ProbeStatusDown { // 检查是否存在未恢复的同探针告警 var existingAlert model.AgentAlert h.db.Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?", r.Name, hostname, "health_probe", false). First(&existingAlert) if existingAlert.ID == 0 { // 获取 fail_count 判断严重级别 var probe model.HealthProbe if err := h.db.First(&probe, r.ProbeID).Error; err == nil { severity := "warning" if probe.FailCount >= 3 { severity = "critical" } alert := model.AgentAlert{ RuleName: r.Name, Hostname: hostname, Metric: "health_probe", Severity: severity, Message: "健康探针 [" + r.Name + "] 检测异常: " + r.Message + " (类型=" + r.ProbeType + ", 延迟=" + strconv.Itoa(r.LatencyMs) + "ms)", Threshold: 0, Resolved: false, } h.db.Create(&alert) } } } else { // 探针恢复正常:自动恢复告警 h.db.Model(&model.AgentAlert{}). Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?", r.Name, hostname, "health_probe", false). Updates(map[string]interface{}{ "resolved": true, "resolved_at": now, }) } } } // probeConfigResponse ReceiveReport 的响应结构(含探针配置) type probeConfigResponse struct { model.Response ProbeConfigs []model.HealthProbe `json:"probe_configs,omitempty"` } // makeProbeConfigResponse 构建带探针配置的响应 func (h *AgentHandler) makeProbeConfigResponse(hostname string, respCode int, respMsg string, probeResults []model.HealthProbeResult) probeConfigResponse { // 处理上报的探针结果 if len(probeResults) > 0 { h.processProbeResults(hostname, probeResults) } // 拉取该主机的新探针配置(供 Agent 下次执行) var probes []model.HealthProbe h.db.Where("hostname = ? AND enabled = ?", hostname, true). Order("id ASC"). Find(&probes) if probes == nil { probes = []model.HealthProbe{} } return probeConfigResponse{ Response: model.Response{ Code: respCode, Message: respMsg, }, ProbeConfigs: probes, } }