Files
aiops/backend/controller/snmp_metrics_history.go
2026-07-09 14:16:56 +08:00

274 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}