package controller import ( "fmt" "net/http" "strconv" "time" "ezops/model" "github.com/gin-gonic/gin" ) // UnifiedAlertItem 统一告警事件视图 // Logan.Li type UnifiedAlertItem struct { ID string `json:"id"` SourceType string `json:"source_type"` RuleName string `json:"rule_name"` Title string `json:"title"` Message string `json:"message"` Severity string `json:"severity"` Status string `json:"status"` Target string `json:"target"` Metric string `json:"metric"` Value float64 `json:"value"` Threshold float64 `json:"threshold"` StartedAt string `json:"started_at"` ResolvedAt *string `json:"resolved_at,omitempty"` } // ListUnifiedAlerts 统一告警列表(主机 + 中间件 + 站点 + 日志) // GET /api/v1/alerts/unified func (h *AlertHandler) ListUnifiedAlerts(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if pageSize > 100 { pageSize = 100 } statusFilter := c.Query("status") severityFilter := c.Query("severity") sourceFilter := c.Query("source_type") items := make([]UnifiedAlertItem, 0) // 1. Agent 主机/中间件/SNMP 告警(中间件与 SNMP 复用 AgentAlert 存储) if sourceFilter == "" || sourceFilter == "host" || sourceFilter == "middleware" || sourceFilter == "snmp" { var alerts []model.AgentAlert q := h.db.Model(&model.AgentAlert{}).Order("created_at DESC").Limit(200) if statusFilter == "firing" || statusFilter == "" { if statusFilter == "firing" { q = q.Where("resolved = ?", false) } } else if statusFilter == "resolved" { q = q.Where("resolved = ?", true) } if severityFilter != "" { q = q.Where("severity = ?", severityFilter) } q.Find(&alerts) for _, a := range alerts { st := "firing" if a.Resolved { st = "resolved" } srcType := "host" if isSNMPMetric(a.Metric) { srcType = "snmp" } else if isMiddlewareMetric(a.Metric) { srcType = "middleware" } if sourceFilter == "host" && srcType != "host" { continue } if sourceFilter == "middleware" && srcType != "middleware" { continue } if sourceFilter == "snmp" && srcType != "snmp" { continue } var resolved *string if a.ResolvedAt != nil { s := a.ResolvedAt.Format(time.RFC3339) resolved = &s } items = append(items, UnifiedAlertItem{ ID: "agent-" + strconv.Itoa(int(a.ID)), SourceType: srcType, RuleName: a.RuleName, Title: a.RuleName, Message: a.Message, Severity: a.Severity, Status: st, Target: a.Hostname, Metric: a.Metric, Value: a.Value, Threshold: a.Threshold, StartedAt: a.CreatedAt.Format(time.RFC3339), ResolvedAt: resolved, }) } } // 2. 站点拨测告警 if sourceFilter == "" || sourceFilter == "site" { var alerts []model.SiteAlert q := h.db.Model(&model.SiteAlert{}).Preload("Task").Order("started_at DESC").Limit(200) if statusFilter == "firing" { q = q.Where("status = ?", model.SiteAlertStatusActive) } else if statusFilter == "resolved" { q = q.Where("status = ?", model.SiteAlertStatusResolved) } if severityFilter != "" { q = q.Where("severity = ?", severityFilter) } q.Find(&alerts) for _, a := range alerts { st := "firing" if a.Status == model.SiteAlertStatusResolved { st = "resolved" } target := fmt.Sprintf("task-%d", a.TaskID) if a.Task != nil { target = a.Task.Name } var resolved *string if a.ResolvedAt != nil { s := a.ResolvedAt.Format(time.RFC3339) resolved = &s } items = append(items, UnifiedAlertItem{ ID: "site-" + strconv.Itoa(int(a.ID)), SourceType: "site", RuleName: a.Title, Title: a.Title, Message: a.Message, Severity: string(a.Severity), Status: st, Target: target, Metric: a.TriggerType, Value: a.ResponseTime, Threshold: a.Threshold, StartedAt: a.StartedAt.Format(time.RFC3339), ResolvedAt: resolved, }) } } // 3. 日志/通用 AlertEvent if sourceFilter == "" || sourceFilter == "log" { var events []model.AlertEvent q := h.db.Model(&model.AlertEvent{}).Order("started_at DESC").Limit(200) if statusFilter != "" && statusFilter != "all" { q = q.Where("status = ?", statusFilter) } if severityFilter != "" { q = q.Where("severity = ?", severityFilter) } q.Find(&events) for _, e := range events { var resolved *string if e.ResolvedAt != nil { s := e.ResolvedAt.Format(time.RFC3339) resolved = &s } items = append(items, UnifiedAlertItem{ ID: "event-" + strconv.Itoa(int(e.ID)), SourceType: "log", RuleName: e.RuleName, Title: e.RuleName, Message: e.Message, Severity: e.Severity, Status: e.Status, Target: e.Target, Metric: "log", Value: e.Value, Threshold: e.Threshold, StartedAt: e.StartedAt.Format(time.RFC3339), ResolvedAt: resolved, }) } } // 按时间排序 sortUnifiedAlerts(items) total := len(items) start := (page - 1) * pageSize if start > total { start = total } end := start + pageSize if end > total { end = total } pageItems := items[start:end] // 统计 stats := gin.H{ "firing": countStatus(items, "firing"), "critical": countSeverityStatus(items, "critical", "firing"), "warning": countSeverityStatus(items, "warning", "firing"), "resolved": countStatus(items, "resolved"), } c.JSON(http.StatusOK, model.Response{ Code: 200, Data: gin.H{"list": pageItems, "total": total, "stats": stats}, }) } func sortUnifiedAlerts(items []UnifiedAlertItem) { for i := 0; i < len(items); i++ { for j := i + 1; j < len(items); j++ { if items[j].StartedAt > items[i].StartedAt { items[i], items[j] = items[j], items[i] } } } } func countStatus(items []UnifiedAlertItem, status string) int { n := 0 for _, it := range items { if it.Status == status { n++ } } return n } func countSeverityStatus(items []UnifiedAlertItem, sev, status string) int { n := 0 for _, it := range items { if it.Severity == sev && it.Status == status { n++ } } return n }