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

+111 -96
View File
@@ -13,10 +13,10 @@ import (
"time"
"ezops/model"
"ezops/pkg/systask"
_ "github.com/go-sql-driver/mysql"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -27,9 +27,29 @@ import (
// MiddlewareConfig 存储在 MonitorTarget.Config JSON 字段中
type MiddlewareConfig struct {
Password string `json:"password,omitempty"`
Username string `json:"username,omitempty"` // MySQL 专用
Database string `json:"database,omitempty"` // MySQL 专用
Password string `json:"password,omitempty"`
Username string `json:"username,omitempty"` // MySQL 专用
Database string `json:"database,omitempty"` // MySQL 专用
RedisMode string `json:"redis_mode,omitempty"` // Redis: standalone / cluster
}
// parseMiddlewareConfig 解析中间件配置并补全默认值 // Logan.Li
func parseMiddlewareConfig(configJSON string) MiddlewareConfig {
var cfg MiddlewareConfig
if configJSON != "" {
json.Unmarshal([]byte(configJSON), &cfg)
}
if cfg.RedisMode == "" {
cfg.RedisMode = "standalone"
}
return cfg
}
// middlewareConfigPublic 返回给前端的脱敏配置 // Logan.Li
type middlewareConfigPublic struct {
Username string `json:"username,omitempty"`
Database string `json:"database,omitempty"`
RedisMode string `json:"redis_mode,omitempty"`
}
// MiddlewareMonitorHandler 中间件监控 handler
@@ -60,11 +80,11 @@ func (h *MiddlewareMonitorHandler) StartCollector(intervalSec int) {
defer ticker.Stop()
log.Printf("[MiddlewareMonitor] 采集引擎启动,间隔 %ds", intervalSec)
// 首次立即采集一次
h.collectAll()
h.collectAllWithRecord()
for {
select {
case <-ticker.C:
h.collectAll()
h.collectAllWithRecord()
case <-h.stopCh:
log.Printf("[MiddlewareMonitor] 采集引擎停止")
return
@@ -87,14 +107,15 @@ func (h *MiddlewareMonitorHandler) StopCollector() {
// POST /api/v1/middleware-monitors
func (h *MiddlewareMonitorHandler) CreateMiddlewareTarget(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"` // redis / mysql
IP string `json:"ip" binding:"required"`
Port int `json:"port" binding:"required"`
Password string `json:"password"`
Username string `json:"username"` // MySQL
Database string `json:"database"` // MySQL
Interval int `json:"interval"` // 采集间隔,默认 30s
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"` // redis / mysql
IP string `json:"ip" binding:"required"`
Port int `json:"port" binding:"required"`
Password string `json:"password"`
Username string `json:"username"` // MySQL
Database string `json:"database"` // MySQL
RedisMode string `json:"redis_mode"` // Redis: standalone / cluster
Interval int `json:"interval"` // 采集间隔,默认 30s
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
@@ -119,11 +140,21 @@ func (h *MiddlewareMonitorHandler) CreateMiddlewareTarget(c *gin.Context) {
req.Interval = 30
}
redisMode := strings.ToLower(req.RedisMode)
if mwType == "redis" {
if redisMode != "cluster" {
redisMode = "standalone"
}
} else {
redisMode = ""
}
// 构建 Config JSON
cfg := MiddlewareConfig{
Password: req.Password,
Username: req.Username,
Database: req.Database,
Password: req.Password,
Username: req.Username,
Database: req.Database,
RedisMode: redisMode,
}
cfgJSON, _ := json.Marshal(cfg)
@@ -139,7 +170,8 @@ func (h *MiddlewareMonitorHandler) CreateMiddlewareTarget(c *gin.Context) {
Description: mwType, // 复用 Description 字段存储中间件子类型
}
if err := h.db.Create(&target).Error; err != nil {
// host_group_id 默认 0 会触发外键约束,创建时显式忽略 // Logan.Li
if err := h.db.Omit("HostGroupID").Create(&target).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
@@ -174,11 +206,12 @@ func (h *MiddlewareMonitorHandler) ListMiddlewareTargets(c *gin.Context) {
targets = []model.MonitorTarget{}
}
// 附带最新指标
// 附带最新指标(Config 脱敏,不返回密码)
type TargetWithMetrics struct {
model.MonitorTarget
SubType string `json:"sub_type"`
LatestMetrics map[string]float64 `json:"latest_metrics"`
SubType string `json:"sub_type"`
LatestMetrics map[string]float64 `json:"latest_metrics"`
ConfigPublic middlewareConfigPublic `json:"config_public"`
}
result := make([]TargetWithMetrics, len(targets))
now := time.Now().Unix()
@@ -193,10 +226,18 @@ func (h *MiddlewareMonitorHandler) ListMiddlewareTargets(c *gin.Context) {
metrics[mh.MetricName] = mh.Value
}
}
cfg := parseMiddlewareConfig(t.Config)
// 列表响应隐藏原始 config 字段,避免泄露密码 // Logan.Li
t.Config = ""
result[i] = TargetWithMetrics{
MonitorTarget: t,
SubType: t.Description,
LatestMetrics: metrics,
ConfigPublic: middlewareConfigPublic{
Username: cfg.Username,
Database: cfg.Database,
RedisMode: cfg.RedisMode,
},
}
}
@@ -237,13 +278,14 @@ func (h *MiddlewareMonitorHandler) UpdateMiddlewareTarget(c *gin.Context) {
}
var req struct {
Name *string `json:"name"`
IP *string `json:"ip"`
Port *int `json:"port"`
Password *string `json:"password"`
Username *string `json:"username"`
Database *string `json:"database"`
Interval *int `json:"interval"`
Name *string `json:"name"`
IP *string `json:"ip"`
Port *int `json:"port"`
Password *string `json:"password"`
Username *string `json:"username"`
Database *string `json:"database"`
RedisMode *string `json:"redis_mode"`
Interval *int `json:"interval"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
@@ -264,12 +306,9 @@ func (h *MiddlewareMonitorHandler) UpdateMiddlewareTarget(c *gin.Context) {
updates["interval"] = *req.Interval
}
// 如果有密码/用户名变更,更新 Config JSON
if req.Password != nil || req.Username != nil || req.Database != nil {
var cfg MiddlewareConfig
if target.Config != "" {
json.Unmarshal([]byte(target.Config), &cfg)
}
// 如果有连接配置变更,更新 Config JSON
if req.Password != nil || req.Username != nil || req.Database != nil || req.RedisMode != nil {
cfg := parseMiddlewareConfig(target.Config)
if req.Password != nil {
cfg.Password = *req.Password
}
@@ -279,6 +318,14 @@ func (h *MiddlewareMonitorHandler) UpdateMiddlewareTarget(c *gin.Context) {
if req.Database != nil {
cfg.Database = *req.Database
}
if req.RedisMode != nil {
mode := strings.ToLower(*req.RedisMode)
if mode == "cluster" {
cfg.RedisMode = "cluster"
} else {
cfg.RedisMode = "standalone"
}
}
cfgJSON, _ := json.Marshal(cfg)
updates["config"] = string(cfgJSON)
}
@@ -363,6 +410,14 @@ func (h *MiddlewareMonitorHandler) GetMiddlewareMetrics(c *gin.Context) {
startTime := now - 3600 // 默认 1h
endTime := now
if h := c.Query("hours"); h != "" {
if hours, err := strconv.Atoi(h); err == nil && hours > 0 {
if hours > 168 {
hours = 168
}
startTime = now - int64(hours)*3600
}
}
if s := c.Query("start"); s != "" {
if v, err := strconv.ParseInt(s, 10, 64); err == nil {
startTime = v
@@ -393,6 +448,14 @@ func (h *MiddlewareMonitorHandler) GetMiddlewareMetrics(c *gin.Context) {
// 指标采集引擎
// ============================================
// collectAllWithRecord 采集并记录系统任务状态 // Logan.Li
func (h *MiddlewareMonitorHandler) collectAllWithRecord() int {
start := time.Now()
count := h.collectAll()
systask.Record("middleware_monitor", "success", fmt.Sprintf("采集 %d 个目标", count), time.Since(start))
return count
}
// collectAll 采集所有启用的中间件目标
func (h *MiddlewareMonitorHandler) collectAll() int {
var targets []model.MonitorTarget
@@ -453,11 +516,7 @@ func (h *MiddlewareMonitorHandler) collectTarget(target model.MonitorTarget) (ma
}
}
// 解析配置
var cfg MiddlewareConfig
if target.Config != "" {
json.Unmarshal([]byte(target.Config), &cfg)
}
cfg := parseMiddlewareConfig(target.Config)
addr := fmt.Sprintf("%s:%d", target.IP, target.Port)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -465,7 +524,7 @@ func (h *MiddlewareMonitorHandler) collectTarget(target model.MonitorTarget) (ma
switch mwType {
case "redis":
return h.collectRedis(ctx, addr, cfg.Password)
return h.collectRedis(ctx, addr, cfg.Password, cfg.RedisMode)
case "mysql":
return h.collectMySQL(ctx, addr, cfg)
default:
@@ -473,62 +532,6 @@ func (h *MiddlewareMonitorHandler) collectTarget(target model.MonitorTarget) (ma
}
}
// collectRedis 采集 Redis 指标
func (h *MiddlewareMonitorHandler) collectRedis(ctx context.Context, addr, password string) (map[string]float64, error) {
rdb := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: 0,
})
defer rdb.Close()
// PING 检测连接
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("Redis 连接失败: %v", err)
}
result := make(map[string]float64)
// INFO 命令获取详细信息
info, err := rdb.Info(ctx).Result()
if err == nil {
infoMap := parseRedisInfo(info)
result["redis_connected_clients"] = parseFloat(infoMap["connected_clients"])
result["redis_used_memory"] = parseFloat(infoMap["used_memory"])
result["redis_used_memory_peak"] = parseFloat(infoMap["used_memory_peak"])
result["redis_total_commands_processed"] = parseFloat(infoMap["total_commands_processed"])
result["redis_uptime_in_seconds"] = parseFloat(infoMap["uptime_in_seconds"])
result["redis_keyspace_hits"] = parseFloat(infoMap["keyspace_hits"])
result["redis_keyspace_misses"] = parseFloat(infoMap["keyspace_misses"])
result["redis_evicted_keys"] = parseFloat(infoMap["evicted_keys"])
result["redis_blocked_clients"] = parseFloat(infoMap["blocked_clients"])
// 计算命中率
hits := result["redis_keyspace_hits"]
misses := result["redis_keyspace_misses"]
if hits+misses > 0 {
result["redis_hit_rate"] = hits / (hits + misses)
} else {
result["redis_hit_rate"] = 0
}
// 计算 QPS
uptime := result["redis_uptime_in_seconds"]
cmds := result["redis_total_commands_processed"]
if uptime > 0 {
result["redis_qps"] = cmds / uptime
}
}
// DBSIZE
dbSize, err := rdb.DBSize(ctx).Result()
if err == nil {
result["redis_db_size"] = float64(dbSize)
}
return result, nil
}
// collectMySQL 采集 MySQL 指标
func (h *MiddlewareMonitorHandler) collectMySQL(ctx context.Context, addr string, cfg MiddlewareConfig) (map[string]float64, error) {
dbName := cfg.Database
@@ -597,6 +600,18 @@ func (h *MiddlewareMonitorHandler) collectMySQL(ctx context.Context, addr string
result["mysql_buffer_hit_rate"] = 0
}
// SHOW GLOBAL VARIABLES — 连接上限等 // Logan.Li
varRows, err := db.QueryContext(ctx, "SHOW GLOBAL VARIABLES LIKE 'max_connections'")
if err == nil {
defer varRows.Close()
for varRows.Next() {
var name, val string
if varRows.Scan(&name, &val) == nil && name == "max_connections" {
result["mysql_max_connections"] = parseFloat(val)
}
}
}
return result, nil
}