package controller import ( "net/http" "strconv" "strings" "ezops/model" "time" "github.com/gin-gonic/gin" ) // ListMonitorAlertRules 列出中间件/站点告警规则 // GET /api/v1/alerts/monitor-rules?category=middleware|site func (h *AlertHandler) ListMonitorAlertRules(c *gin.Context) { q := h.db.Model(&model.MonitorAlertRule{}) if cat := c.Query("category"); cat != "" { q = q.Where("category = ?", cat) } var rules []model.MonitorAlertRule var total int64 q.Count(&total) q.Order("id DESC").Find(&rules) c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": rules, "total": total}}) } // CreateMonitorAlertRule 创建中间件/站点告警规则 // POST /api/v1/alerts/monitor-rules func (h *AlertHandler) CreateMonitorAlertRule(c *gin.Context) { var raw map[string]interface{} if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } rule := parseMonitorAlertRule(raw) if rule.Name == "" || rule.Metric == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "名称和指标不能为空"}) return } if rule.Category != model.MonitorAlertMiddleware && rule.Category != model.MonitorAlertSite && rule.Category != model.MonitorAlertSNMP { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "category 须为 middleware、snmp 或 site"}) return } if err := h.db.Create(&rule).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: rule, Message: "创建成功"}) } // UpdateMonitorAlertRule 更新规则 // PUT /api/v1/alerts/monitor-rules/:id func (h *AlertHandler) UpdateMonitorAlertRule(c *gin.Context) { id := c.Param("id") var rule model.MonitorAlertRule if err := h.db.First(&rule, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "规则不存在"}) return } var raw map[string]interface{} if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } updates := buildMonitorRuleUpdates(raw) if len(updates) > 0 { h.db.Model(&rule).Updates(updates) h.db.First(&rule, id) } c.JSON(http.StatusOK, model.Response{Code: 200, Data: rule, Message: "更新成功"}) } // DeleteMonitorAlertRule 删除规则 // DELETE /api/v1/alerts/monitor-rules/:id func (h *AlertHandler) DeleteMonitorAlertRule(c *gin.Context) { id := c.Param("id") if err := h.db.Delete(&model.MonitorAlertRule{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // ListUnifiedAlertRules 统一规则列表(主机 AgentRule + 中间件/站点 MonitorAlertRule) // GET /api/v1/alerts/unified-rules?category=host|middleware|site|all func (h *AlertHandler) ListUnifiedAlertRules(c *gin.Context) { cat := c.DefaultQuery("category", "all") list := make([]gin.H, 0) if cat == "all" || cat == "host" { var agentRules []model.AgentRule h.db.Order("id DESC").Find(&agentRules) for _, r := range agentRules { list = append(list, gin.H{ "id": r.ID, "category": "host", "name": r.Name, "target": r.Hostname, "metric": r.Metric, "operator": r.Operator, "threshold": r.Threshold, "port": r.Port, "enabled": r.Enabled, "severity": "warning", "description": r.Hostname + " " + r.Metric, "rule_type": "agent", }) } } if cat == "all" || cat == "middleware" || cat == "site" || cat == "snmp" { q := h.db.Model(&model.MonitorAlertRule{}) if cat != "all" { q = q.Where("category = ?", cat) } var rules []model.MonitorAlertRule q.Order("id DESC").Find(&rules) for _, r := range rules { list = append(list, gin.H{ "id": r.ID, "category": r.Category, "name": r.Name, "target_id": r.TargetID, "metric": r.Metric, "operator": r.Operator, "threshold": r.Threshold, "enabled": r.Enabled, "severity": r.Severity, "description": r.Description, "rule_type": "monitor", }) } } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": list, "total": len(list)}}) } func parseMonitorAlertRule(raw map[string]interface{}) model.MonitorAlertRule { getStr := func(keys ...string) string { for _, k := range keys { if v, ok := raw[k].(string); ok { return strings.TrimSpace(v) } } return "" } getFloat := func(def float64, keys ...string) float64 { for _, k := range keys { if v, ok := raw[k].(float64); ok { return v } } return def } getUint := func(keys ...string) uint { for _, k := range keys { switch v := raw[k].(type) { case float64: return uint(v) case int: return uint(v) } } return 0 } rule := model.MonitorAlertRule{ Category: model.MonitorAlertCategory(getStr("category")), Name: getStr("name"), Metric: getStr("metric"), Operator: getStr("operator", "op"), Threshold: getFloat(0, "threshold"), Severity: getStr("severity"), Description: getStr("description"), TargetID: getUint("target_id"), Enabled: true, } if rule.Operator == "" { rule.Operator = ">" } if rule.Severity == "" { rule.Severity = "warning" } if v, ok := raw["enabled"].(bool); ok { rule.Enabled = v } return rule } func buildMonitorRuleUpdates(raw map[string]interface{}) map[string]interface{} { updates := map[string]interface{}{} if v, ok := raw["name"].(string); ok && strings.TrimSpace(v) != "" { updates["name"] = strings.TrimSpace(v) } if v, ok := raw["metric"].(string); ok && v != "" { updates["metric"] = v } if v, ok := raw["operator"].(string); ok && v != "" { updates["operator"] = v } if v, ok := raw["threshold"].(float64); ok { updates["threshold"] = v } if v, ok := raw["severity"].(string); ok && v != "" { updates["severity"] = v } if v, ok := raw["description"].(string); ok { updates["description"] = v } if v, ok := raw["enabled"].(bool); ok { updates["enabled"] = v } if v, ok := raw["target_id"].(float64); ok { updates["target_id"] = uint(v) } return updates } // QuickSetupMonitorRules 一键创建中间件/站点默认告警规则 // POST /api/v1/alerts/monitor-quick-setup func (h *AlertHandler) QuickSetupMonitorRules(c *gin.Context) { var req struct { Category string `json:"category"` } c.ShouldBindJSON(&req) cat := req.Category if cat == "" { cat = "all" } created := 0 if cat == "all" || cat == "middleware" { defaults := []model.MonitorAlertRule{ {Category: model.MonitorAlertMiddleware, Name: "Redis 连接数过高", Metric: "redis_connected_clients", Operator: ">", Threshold: 500, Severity: "warning", Enabled: true, Description: "Redis 连接数 > 500"}, {Category: model.MonitorAlertMiddleware, Name: "Redis 命中率过低", Metric: "redis_hit_rate", Operator: "<", Threshold: 0.9, Severity: "warning", Enabled: true, Description: "命中率 < 90%"}, {Category: model.MonitorAlertMiddleware, Name: "MySQL 连接数过高", Metric: "mysql_connections", Operator: ">", Threshold: 200, Severity: "warning", Enabled: true, Description: "连接数 > 200"}, {Category: model.MonitorAlertMiddleware, Name: "MySQL 慢查询过多", Metric: "mysql_slow_queries", Operator: ">", Threshold: 10, Severity: "warning", Enabled: true, Description: "慢查询 > 10"}, } for _, r := range defaults { var exist model.MonitorAlertRule h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist) if exist.ID == 0 { h.db.Create(&r) created++ } } } if cat == "all" || cat == "site" { defaults := []model.MonitorAlertRule{ {Category: model.MonitorAlertSite, Name: "拨测失败", Metric: "probe_fail", Operator: "==", Threshold: 1, Severity: "critical", Enabled: true, Description: "拨测连续失败"}, {Category: model.MonitorAlertSite, Name: "响应延迟过高", Metric: "response_time", Operator: ">", Threshold: 500, Severity: "warning", Enabled: true, Description: "延迟 > 500ms"}, {Category: model.MonitorAlertSite, Name: "可用率过低", Metric: "availability", Operator: "<", Threshold: 99, Severity: "warning", Enabled: true, Description: "1h 可用率 < 99%"}, {Category: model.MonitorAlertSite, Name: "SSL 证书即将过期", Metric: "ssl_expiry_days", Operator: "<", Threshold: 30, Severity: "warning", Enabled: true, Description: "SSL 拨测证书剩余 < 30 天"}, } for _, r := range defaults { var exist model.MonitorAlertRule h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist) if exist.ID == 0 { h.db.Create(&r) created++ } } } if cat == "all" || cat == "snmp" { defaults := []model.MonitorAlertRule{ {Category: model.MonitorAlertSNMP, Name: "SNMP CPU 过高", Metric: "snmp_cpu_usage", Operator: ">", Threshold: 80, Severity: "warning", Enabled: true, Description: "CPU > 80%"}, {Category: model.MonitorAlertSNMP, Name: "SNMP 内存过高", Metric: "snmp_mem_usage", Operator: ">", Threshold: 85, Severity: "warning", Enabled: true, Description: "内存 > 85%"}, {Category: model.MonitorAlertSNMP, Name: "SNMP 温度过高", Metric: "snmp_temperature", Operator: ">", Threshold: 65, Severity: "warning", Enabled: true, Description: "温度 > 65°C"}, {Category: model.MonitorAlertSNMP, Name: "SNMP 设备离线", Metric: "snmp_offline", Operator: "==", Threshold: 1, Severity: "critical", Enabled: true, Description: "设备离线或采集失败"}, } for _, r := range defaults { var exist model.MonitorAlertRule h.db.Where("name = ? AND category = ?", r.Name, r.Category).First(&exist) if exist.ID == 0 { h.db.Create(&r) created++ } } } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"created": created}, Message: "默认规则已创建"}) } // ResolveUnifiedAlert 统一告警恢复 // POST /api/v1/alerts/unified/:source/:id/resolve func (h *AlertHandler) ResolveUnifiedAlert(c *gin.Context) { source := c.Param("source") id, _ := strconv.Atoi(c.Param("id")) now := time.Now() switch source { case "agent": h.db.Model(&model.AgentAlert{}).Where("id = ?", id).Updates(map[string]interface{}{"resolved": true, "resolved_at": now}) case "site": h.db.Model(&model.SiteAlert{}).Where("id = ?", id).Updates(map[string]interface{}{"status": model.SiteAlertStatusResolved, "resolved_at": now}) case "event": h.db.Model(&model.AlertEvent{}).Where("id = ?", id).Updates(map[string]interface{}{"status": model.AlertEventResolved, "resolved_at": now}) default: c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未知告警来源"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复"}) }