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

+273
View File
@@ -0,0 +1,273 @@
package controller
import (
"net/http"
"strconv"
"time"
"ezops/model"
snmppkg "ezops/pkg/snmp"
"github.com/gin-gonic/gin"
)
// snmpChartableNames 适合折线图展示的标量指标 // Logan.Li
var snmpChartableNames = []string{
"h3c_cpu_usage", "hw_cpu_usage", "cisco_cpu_5min", "ruijie_cpu_usage", "inspur_cpu_usage",
"h3c_mem_usage", "hw_mem_usage", "ruijie_mem_usage", "inspur_mem_usage",
"h3c_temperature", "hw_temperature", "ruijie_temperature", "dell_temperature", "inspur_temperature",
"h3c_fan_speed", "hw_fan_speed", "ruijie_fan_speed", "dell_fan_speed", "inspur_fan_speed",
"cisco_sensor_value", "ent_sensor_value",
"if_in_octets", "if_out_octets",
}
// snmpCounterMetrics 累计型 counter,历史查询需差分换算速率 // Logan.Li
var snmpCounterMetrics = map[string]bool{
"if_in_octets": true,
"if_out_octets": true,
}
type snmpMetricSeries struct {
Name string `json:"name"`
Index string `json:"index,omitempty"`
Unit string `json:"unit,omitempty"`
Label string `json:"label"`
LatestValue float64 `json:"latest_value"`
}
type snmpHistoryPoint struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Unit string `json:"unit,omitempty"`
}
type snmpOverviewItem struct {
DeviceID uint `json:"device_id"`
Name string `json:"name"`
IP string `json:"ip"`
Status string `json:"status"`
CPU float64 `json:"cpu,omitempty"`
Mem float64 `json:"mem,omitempty"`
Temp float64 `json:"temp,omitempty"`
}
// ListMetricSeries GET /snmp/devices/:id/metrics/series — 可绘图指标序列 // Logan.Li
func (h *SNMPHandler) ListMetricSeries(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var device model.SNMPDevice
if err := h.db.First(&device, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "设备不存在"})
return
}
var latestTS int64
h.db.Model(&model.SNMPMetric{}).Where("device_id = ?", id).
Select("COALESCE(MAX(timestamp), 0)").Scan(&latestTS)
if latestTS == 0 {
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": []snmpMetricSeries{}, "total": 0}})
return
}
type row struct {
Name string
Index string
Unit string
LatestValue float64
}
var rows []row
h.db.Model(&model.SNMPMetric{}).
Select("name, `index`, unit, MAX(value) as latest_value").
Where("device_id = ? AND timestamp = ? AND name IN ?", id, latestTS, snmpChartableNames).
Group("name, `index`, unit").
Order("name ASC, `index` ASC").
Scan(&rows)
series := make([]snmpMetricSeries, 0, len(rows))
for _, r := range rows {
label := snmpMetricLabel(r.Name, r.Index)
series = append(series, snmpMetricSeries{
Name: r.Name, Index: r.Index, Unit: r.Unit,
Label: label, LatestValue: r.LatestValue,
})
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": series, "total": len(series)}})
}
// QueryMetricHistory GET /snmp/devices/:id/metrics/history — 指标时序 // Logan.Li
func (h *SNMPHandler) QueryMetricHistory(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
metricName := c.Query("metric_name")
if metricName == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "metric_name 不能为空"})
return
}
index := c.Query("index")
hours, _ := strconv.Atoi(c.DefaultQuery("hours", "168"))
if hours <= 0 {
hours = 168
}
if hours > snmppkg.MetricsRetentionDays*24 {
hours = snmppkg.MetricsRetentionDays * 24
}
fromTS := time.Now().Add(-time.Duration(hours) * time.Hour).Unix()
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "500"))
if limit <= 0 || limit > 2000 {
limit = 500
}
q := h.db.Model(&model.SNMPMetric{}).
Where("device_id = ? AND name = ? AND timestamp >= ?", id, metricName, fromTS)
if index != "" {
q = q.Where("`index` = ?", index)
}
var points []snmpHistoryPoint
if index != "" {
var metrics []model.SNMPMetric
q.Order("timestamp ASC").Limit(limit).Find(&metrics)
points = make([]snmpHistoryPoint, 0, len(metrics))
for _, m := range metrics {
points = append(points, snmpHistoryPoint{
Timestamp: m.Timestamp, Value: m.Value, Unit: m.Unit,
})
}
} else {
type aggRow struct {
Timestamp int64
Value float64
Unit string
}
var rows []aggRow
h.db.Model(&model.SNMPMetric{}).
Select("timestamp, AVG(value) as value, MAX(unit) as unit").
Where("device_id = ? AND name = ? AND timestamp >= ?", id, metricName, fromTS).
Group("timestamp").
Order("timestamp ASC").
Limit(limit).
Scan(&rows)
points = make([]snmpHistoryPoint, 0, len(rows))
for _, r := range rows {
points = append(points, snmpHistoryPoint{
Timestamp: r.Timestamp, Value: r.Value, Unit: r.Unit,
})
}
}
if points == nil {
points = []snmpHistoryPoint{}
}
// counter 指标差分换算为 bps(bits per second)// Logan.Li
rateUnit := ""
if snmpCounterMetrics[metricName] && len(points) > 1 {
points = snmpCounterToRate(points)
rateUnit = "bps"
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"list": points, "total": len(points),
"metric_name": metricName, "index": index, "hours": hours,
"rate_unit": rateUnit,
}})
}
// snmpCounterToRate 将累计字节 counter 差分为比特速率 // Logan.Li
func snmpCounterToRate(points []snmpHistoryPoint) []snmpHistoryPoint {
if len(points) < 2 {
return points
}
out := make([]snmpHistoryPoint, 0, len(points)-1)
for i := 1; i < len(points); i++ {
prev, cur := points[i-1], points[i]
dt := float64(cur.Timestamp - prev.Timestamp)
if dt <= 0 {
continue
}
delta := cur.Value - prev.Value
if delta < 0 {
// counter 回绕,跳过该点 // Logan.Li
continue
}
bps := (delta * 8) / dt
out = append(out, snmpHistoryPoint{
Timestamp: cur.Timestamp,
Value: bps,
Unit: "bps",
})
}
if len(out) == 0 {
return []snmpHistoryPoint{}
}
return out
}
// GetOverviewMetrics GET /snmp/overview/metrics — 各设备最新关键指标(图表概览) // Logan.Li
func (h *SNMPHandler) GetOverviewMetrics(c *gin.Context) {
var devices []model.SNMPDevice
h.db.Where("template_id > 0").Order("name ASC").Find(&devices)
cpuNames := map[string]bool{
"h3c_cpu_usage": true, "hw_cpu_usage": true, "cisco_cpu_5min": true,
"ruijie_cpu_usage": true, "inspur_cpu_usage": true,
}
memNames := map[string]bool{
"h3c_mem_usage": true, "hw_mem_usage": true, "ruijie_mem_usage": true, "inspur_mem_usage": true,
}
tempNames := map[string]bool{
"h3c_temperature": true, "hw_temperature": true, "ruijie_temperature": true,
"dell_temperature": true, "inspur_temperature": true, "cisco_sensor_value": true,
}
items := make([]snmpOverviewItem, 0, len(devices))
for _, d := range devices {
item := snmpOverviewItem{
DeviceID: d.ID, Name: d.Name, IP: d.IP, Status: string(d.Status),
}
var latestTS int64
h.db.Model(&model.SNMPMetric{}).Where("device_id = ?", d.ID).
Select("COALESCE(MAX(timestamp), 0)").Scan(&latestTS)
if latestTS == 0 {
items = append(items, item)
continue
}
var metrics []model.SNMPMetric
h.db.Where("device_id = ? AND timestamp = ? AND name IN ?", d.ID, latestTS, snmpChartableNames).
Find(&metrics)
for _, m := range metrics {
switch {
case cpuNames[m.Name] && m.Value > item.CPU:
item.CPU = m.Value
case memNames[m.Name] && m.Value > item.Mem:
item.Mem = m.Value
case tempNames[m.Name] && m.Value > item.Temp:
item.Temp = m.Value
}
}
items = append(items, item)
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": items, "total": len(items)}})
}
func snmpMetricLabel(name, index string) string {
labels := map[string]string{
"h3c_cpu_usage": "H3C CPU", "hw_cpu_usage": "华为 CPU", "cisco_cpu_5min": "Cisco CPU",
"ruijie_cpu_usage": "锐捷 CPU", "inspur_cpu_usage": "浪潮 CPU",
"h3c_mem_usage": "H3C 内存", "hw_mem_usage": "华为 内存",
"ruijie_mem_usage": "锐捷 内存", "inspur_mem_usage": "浪潮 内存",
"h3c_temperature": "H3C 温度", "hw_temperature": "华为 温度",
"ruijie_temperature": "锐捷 温度", "dell_temperature": "Dell 温度",
"inspur_temperature": "浪潮 温度", "cisco_sensor_value": "Cisco 传感器",
"h3c_fan_speed": "H3C 风扇", "hw_fan_speed": "华为 风扇",
"ruijie_fan_speed": "锐捷 风扇", "dell_fan_speed": "Dell 风扇", "inspur_fan_speed": "浪潮 风扇",
"ent_sensor_value": "实体传感器",
"if_in_octets": "入方向流量", "if_out_octets": "出方向流量",
}
label := labels[name]
if label == "" {
label = name
}
if index != "" {
label += " #" + index
}
return label
}