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>
This commit is contained in:
1 parent
092d0e4b9e
commit
471353e3d7
190 files changed
+16640
-3484
No files matched your search
@@ -0,0 +1,215 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user