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,201 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
snmppkg "ezops/pkg/snmp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gosnmp/gosnmp"
|
||||
)
|
||||
|
||||
// collectDevice 采集单台 SNMP 设备并落库 // Logan.Li
|
||||
func (h *SNMPHandler) collectDevice(device *model.SNMPDevice) error {
|
||||
if device.TemplateID == 0 {
|
||||
return fmt.Errorf("未绑定 OID 模板")
|
||||
}
|
||||
var oids []model.SNMPTemplateOID
|
||||
h.db.Where("template_id = ?", device.TemplateID).Order("sort_order ASC").Find(&oids)
|
||||
if len(oids) == 0 {
|
||||
return fmt.Errorf("模板无 OID 定义")
|
||||
}
|
||||
|
||||
col, err := h.newCollector(device)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defs := make([]snmppkg.OIDDefinition, 0, len(oids))
|
||||
for _, o := range oids {
|
||||
defs = append(defs, snmppkg.OIDDefinition{
|
||||
Name: o.Name, OID: o.OID, Unit: o.Unit,
|
||||
DataType: o.DataType, IsTable: o.Conversion == "table",
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(device.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
result := col.Collect(ctx, defs)
|
||||
|
||||
now := time.Now()
|
||||
ts := now.Unix()
|
||||
updates := map[string]interface{}{
|
||||
"last_collect_at": now,
|
||||
}
|
||||
if result.Success {
|
||||
updates["status"] = model.SNMPDeviceOnline
|
||||
updates["last_error"] = ""
|
||||
for _, m := range result.Metrics {
|
||||
h.db.Create(&model.SNMPMetric{
|
||||
DeviceID: device.ID, Name: m.Name, OID: m.OID, Index: m.Index,
|
||||
Value: m.Value, RawValue: m.RawValue, Unit: m.Unit,
|
||||
DataType: m.DataType, Timestamp: ts,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
updates["status"] = model.SNMPDeviceError
|
||||
errMsg := result.Error
|
||||
if errMsg == "" {
|
||||
errMsg = "采集无数据"
|
||||
}
|
||||
updates["last_error"] = errMsg
|
||||
}
|
||||
h.db.Model(device).Updates(updates)
|
||||
if !result.Success {
|
||||
return fmt.Errorf("%s", updates["last_error"])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SNMPHandler) newCollector(device *model.SNMPDevice) (*snmppkg.Collector, error) {
|
||||
timeout := device.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 10
|
||||
}
|
||||
retries := device.Retries
|
||||
if retries <= 0 {
|
||||
retries = 2
|
||||
}
|
||||
port := uint16(device.Port)
|
||||
if port == 0 {
|
||||
port = 161
|
||||
}
|
||||
cfg := &snmppkg.CollectorConfig{
|
||||
IP: device.IP, Port: port, Community: device.Community,
|
||||
Timeout: time.Duration(timeout) * time.Second, Retries: retries,
|
||||
MaxRepetitions: uint8(device.MaxRepetitions),
|
||||
}
|
||||
switch device.Version {
|
||||
case model.SNMPVersionV1:
|
||||
cfg.Version = gosnmp.Version1
|
||||
case model.SNMPVersionV3:
|
||||
cfg.Version = gosnmp.Version3
|
||||
default:
|
||||
cfg.Version = gosnmp.Version2c
|
||||
}
|
||||
return snmppkg.NewCollector(cfg)
|
||||
}
|
||||
|
||||
// deviceCollectInterval 设备采集间隔,不低于全局最低值 // Logan.Li
|
||||
func deviceCollectInterval(device *model.SNMPDevice) int {
|
||||
interval := device.CollectInterval
|
||||
if interval <= 0 {
|
||||
interval = snmppkg.DefaultCollectIntervalSec
|
||||
}
|
||||
if interval < snmppkg.MinCollectIntervalSec {
|
||||
interval = snmppkg.MinCollectIntervalSec
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
// shouldCollectDevice 判断是否到达该设备的采集周期 // Logan.Li
|
||||
func shouldCollectDevice(device *model.SNMPDevice, now time.Time) bool {
|
||||
if device.LastCollectAt == nil {
|
||||
return true
|
||||
}
|
||||
interval := time.Duration(deviceCollectInterval(device)) * time.Second
|
||||
return now.Sub(*device.LastCollectAt) >= interval
|
||||
}
|
||||
|
||||
// StartCollector 启动 SNMP 后台采集 // Logan.Li
|
||||
func (h *SNMPHandler) StartCollector(intervalSec int) {
|
||||
if intervalSec < snmppkg.MinCollectIntervalSec {
|
||||
intervalSec = snmppkg.DefaultCollectIntervalSec
|
||||
}
|
||||
h.wg.Add(1)
|
||||
go func() {
|
||||
defer h.wg.Done()
|
||||
ticker := time.NewTicker(time.Duration(intervalSec) * time.Second)
|
||||
defer ticker.Stop()
|
||||
log.Printf("[SNMP] 采集引擎启动,间隔 %ds", intervalSec)
|
||||
h.collectAll()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
h.collectAll()
|
||||
case <-h.stopCh:
|
||||
log.Printf("[SNMP] 采集引擎停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *SNMPHandler) collectAll() {
|
||||
var devices []model.SNMPDevice
|
||||
h.db.Where("template_id > 0").Find(&devices)
|
||||
now := time.Now()
|
||||
for i := range devices {
|
||||
if !shouldCollectDevice(&devices[i], now) {
|
||||
continue
|
||||
}
|
||||
if err := h.collectDevice(&devices[i]); err != nil {
|
||||
log.Printf("[SNMP] 采集失败 %s(%s): %v", devices[i].Name, devices[i].IP, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CollectDeviceNow POST /snmp/devices/:id/collect
|
||||
func (h *SNMPHandler) CollectDeviceNow(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
|
||||
}
|
||||
if err := h.collectDevice(&device); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.First(&device, id)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: device, Message: "采集成功"})
|
||||
}
|
||||
|
||||
// ListDeviceMetrics GET /snmp/devices/:id/metrics(仅返回最近一次采集批次) // Logan.Li
|
||||
func (h *SNMPHandler) ListDeviceMetrics(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var latestTS int64
|
||||
h.db.Model(&model.SNMPMetric{}).Where("device_id = ?", id).
|
||||
Select("COALESCE(MAX(timestamp), 0)").Scan(&latestTS)
|
||||
var metrics []model.SNMPMetric
|
||||
if latestTS > 0 {
|
||||
h.db.Where("device_id = ? AND timestamp = ?", id, latestTS).
|
||||
Order("name ASC, `index` ASC").Limit(500).Find(&metrics)
|
||||
}
|
||||
if metrics == nil {
|
||||
metrics = []model.SNMPMetric{}
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": metrics, "total": len(metrics)}})
|
||||
}
|
||||
|
||||
// StopCollector 停止采集引擎 // Logan.Li
|
||||
func (h *SNMPHandler) StopCollector() {
|
||||
close(h.stopCh)
|
||||
h.wg.Wait()
|
||||
}
|
||||
Reference in new issue
Block a user