Files
aiops/backend/controller/alert_unified.go
T
LoganandClaude 471353e3d7 feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心
- 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog)
- 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则
- 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp)

监控采集
- SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect)
- 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed)
- 站点探测 pkg(siteprobe) + 站点视图(site_view)

自动化任务
- cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task)

审计与凭证
- 审计设置(audit_setting) + 历史清理(auditcleaner)

前端
- 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化
- 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染
- 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 23:44:45 +08:00

215 lines
5.9 KiB
Go

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
}