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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user