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,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: "已恢复"})
|
||||
}
|
||||
Reference in new issue
Block a user