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:
LoganandClaude committed 2026-07-01 23:44:45 +08:00
1 parent 092d0e4b9e
commit 471353e3d7
190 files changed
+16640 -3484

No files matched your search

+285
View File
@@ -0,0 +1,285 @@
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: "已恢复"})
}