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