Files
aiops/backend/controller/snmp_collect.go
T
LoganandClaude 471353e3d7 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>
2026-07-01 23:44:45 +08:00

201 lines
5.4 KiB
Go

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()
}