feat: 重构监控中心导航,新增资源监控4大模块
- 主菜单「监控告警」→「监控中心」 - 「告警中心」→「告警服务」移至链路追踪下方 - 新增资源监控 Tab 页(主机/中间件/SNMP/站点拨测) - Agent 新增网络采集器(network)和磁盘IO采集器(diskio) - 后端扩展 Agent 上报模型,支持 NetworkStat/DiskIOStat - 前端 ServerTableRow 展示网络和磁盘IO实时指标 - WebSocket MetricsUpdate 推送扩展
This commit is contained in:
1 parent
020b7a9063
commit
f3a9e5e8d0
21 files changed
+1007
-32
No files matched your search
@@ -0,0 +1,121 @@
|
||||
// Package collector — 磁盘 IO 指标采集器
|
||||
// 通过读取 /proc/diskstats 获取磁盘读写 BPS 和 IOPS。
|
||||
//
|
||||
// 署名 Hermes
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ezops/agent/model"
|
||||
)
|
||||
|
||||
// diskIOSample 保存上一次采样,用于计算速率
|
||||
type diskIOSample struct {
|
||||
time time.Time
|
||||
readSects uint64
|
||||
writeSects uint64
|
||||
readOps uint64
|
||||
writeOps uint64
|
||||
}
|
||||
|
||||
// DiskIOCollector 磁盘 IO 采集器
|
||||
type DiskIOCollector struct {
|
||||
mu sync.Mutex
|
||||
prev *diskIOSample
|
||||
}
|
||||
|
||||
// NewDiskIOCollector 创建磁盘 IO 采集器
|
||||
func NewDiskIOCollector() *DiskIOCollector {
|
||||
return &DiskIOCollector{}
|
||||
}
|
||||
|
||||
// Collect 采集磁盘 IO 指标
|
||||
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
||||
stat := model.DiskIOStat{}
|
||||
|
||||
rSects, wSects, rOps, wOps, err := readDiskStats()
|
||||
if err != nil {
|
||||
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cur := &diskIOSample{
|
||||
time: now,
|
||||
readSects: rSects,
|
||||
writeSects: wSects,
|
||||
readOps: rOps,
|
||||
writeOps: wOps,
|
||||
}
|
||||
|
||||
if c.prev != nil && now.After(c.prev.time) {
|
||||
elapsed := now.Sub(c.prev.time).Seconds()
|
||||
if elapsed > 0 {
|
||||
sectorSize := uint64(512) // 标准扇区 512 bytes
|
||||
stat.ReadBPS = float64((cur.readSects-c.prev.readSects)*sectorSize) / elapsed
|
||||
stat.WriteBPS = float64((cur.writeSects-c.prev.writeSects)*sectorSize) / elapsed
|
||||
stat.ReadIOPS = float64(cur.readOps-c.prev.readOps) / elapsed
|
||||
stat.WriteIOPS = float64(cur.writeOps-c.prev.writeOps) / elapsed
|
||||
}
|
||||
}
|
||||
c.prev = cur
|
||||
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
|
||||
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数
|
||||
func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
|
||||
f, err := os.Open("/proc/diskstats")
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 14 {
|
||||
continue
|
||||
}
|
||||
|
||||
dev := fields[2]
|
||||
// 只采集物理磁盘 (sdX, nvmeXn1, vdX),跳过分区和 loop 设备
|
||||
if strings.HasPrefix(dev, "loop") ||
|
||||
(strings.HasPrefix(dev, "sd") && len(dev) > 3) || // sda1, sda2 等分区
|
||||
(strings.HasPrefix(dev, "nvme") && strings.Contains(dev, "p")) || // nvme0n1p1
|
||||
(strings.HasPrefix(dev, "ram")) ||
|
||||
strings.HasPrefix(dev, "dm-") {
|
||||
continue
|
||||
}
|
||||
|
||||
// field 3 = 读完成次数 (rd_ios)
|
||||
// field 5 = 读扇区数 (rd_sectors)
|
||||
// field 7 = 写完成次数 (wr_ios)
|
||||
// field 9 = 写扇区数 (wr_sectors)
|
||||
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
|
||||
rOps += v
|
||||
}
|
||||
if v, e := strconv.ParseUint(fields[5], 10, 64); e == nil {
|
||||
rSects += v
|
||||
}
|
||||
if v, e := strconv.ParseUint(fields[7], 10, 64); e == nil {
|
||||
wOps += v
|
||||
}
|
||||
if v, e := strconv.ParseUint(fields[9], 10, 64); e == nil {
|
||||
wSects += v
|
||||
}
|
||||
}
|
||||
|
||||
return rSects, wSects, rOps, wOps, scanner.Err()
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Package collector 提供服务器各项系统指标的采集能力。
|
||||
// 本文件实现网络连接数、网卡流量等网络指标采集。
|
||||
//
|
||||
// 署名 Hermes (extended from Logan.Li pattern)
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ezops/agent/model"
|
||||
)
|
||||
|
||||
// netSample 保存上一次的网络采样数据,用于计算速率
|
||||
type netSample struct {
|
||||
time time.Time
|
||||
rxBytes uint64
|
||||
txBytes uint64
|
||||
}
|
||||
|
||||
// NetworkCollector 网络指标采集器
|
||||
type NetworkCollector struct {
|
||||
mu sync.Mutex
|
||||
prev *netSample
|
||||
}
|
||||
|
||||
// NewNetworkCollector 创建网络采集器
|
||||
func NewNetworkCollector() *NetworkCollector {
|
||||
return &NetworkCollector{}
|
||||
}
|
||||
|
||||
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s)
|
||||
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
|
||||
stat := model.NetworkStat{}
|
||||
|
||||
// 1. 采集 TCP 连接统计
|
||||
if tcpStat, err := readSockStat(); err == nil {
|
||||
stat.TCPEstablished = tcpStat.established
|
||||
stat.TCPTimeWait = tcpStat.timeWait
|
||||
stat.TCPTotal = tcpStat.total
|
||||
}
|
||||
|
||||
// 2. 采集网卡流量(取所有非 lo 网卡合计)
|
||||
curRx, curTx, nicName, err := readNetDevTotal()
|
||||
if err != nil {
|
||||
return stat, fmt.Errorf("读取网卡流量失败: %w", err)
|
||||
}
|
||||
stat.NicName = nicName
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cur := &netSample{time: now, rxBytes: curRx, txBytes: curTx}
|
||||
|
||||
if c.prev != nil && now.After(c.prev.time) {
|
||||
elapsed := now.Sub(c.prev.time).Seconds()
|
||||
if elapsed > 0 && cur.rxBytes >= c.prev.rxBytes && cur.txBytes >= c.prev.txBytes {
|
||||
stat.NetRxBPS = float64(cur.rxBytes-c.prev.rxBytes) / elapsed
|
||||
stat.NetTxBPS = float64(cur.txBytes-c.prev.txBytes) / elapsed
|
||||
}
|
||||
}
|
||||
c.prev = cur
|
||||
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// tcpSockStat 解析 /proc/net/sockstat 或 /proc/net/tcp 的结果
|
||||
type tcpSockStat struct {
|
||||
established int
|
||||
timeWait int
|
||||
total int
|
||||
}
|
||||
|
||||
// readSockStat 从 /proc/net/sockstat 读取 TCP 连接统计
|
||||
func readSockStat() (tcpSockStat, error) {
|
||||
var result tcpSockStat
|
||||
|
||||
f, err := os.Open("/proc/net/sockstat")
|
||||
if err != nil {
|
||||
// Fallback: try /proc/net/sockstat6
|
||||
return result, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "TCP:") {
|
||||
continue
|
||||
}
|
||||
// 格式: "TCP: inuse 123 orphan 0 tw 45 alloc 200 mem 30"
|
||||
fields := strings.Fields(line)
|
||||
for i, f := range fields {
|
||||
switch f {
|
||||
case "inuse":
|
||||
if i+1 < len(fields) {
|
||||
result.established, _ = strconv.Atoi(fields[i+1])
|
||||
}
|
||||
case "tw":
|
||||
if i+1 < len(fields) {
|
||||
result.timeWait, _ = strconv.Atoi(fields[i+1])
|
||||
}
|
||||
case "alloc":
|
||||
if i+1 < len(fields) {
|
||||
result.total, _ = strconv.Atoi(fields[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
result.total = result.established // total = established count (inuse)
|
||||
break
|
||||
}
|
||||
|
||||
return result, scanner.Err()
|
||||
}
|
||||
|
||||
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量
|
||||
func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
|
||||
f, err := os.Open("/proc/net/dev")
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
// Skip header lines (first 2 lines)
|
||||
lineNum := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
if lineNum <= 2 {
|
||||
continue
|
||||
}
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(parts[0])
|
||||
// Skip loopback and virtual interfaces
|
||||
if name == "lo" || strings.HasPrefix(name, "veth") || strings.HasPrefix(name, "docker") ||
|
||||
strings.HasPrefix(name, "br-") || strings.HasPrefix(name, "virbr") {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(parts[1])
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
// fields[0]=rx_bytes, fields[8]=tx_bytes
|
||||
rb, _ := strconv.ParseUint(fields[0], 10, 64)
|
||||
tb, _ := strconv.ParseUint(fields[8], 10, 64)
|
||||
|
||||
if rb > 0 || tb > 0 {
|
||||
nicName = name
|
||||
rxBytes += rb
|
||||
txBytes += tb
|
||||
}
|
||||
}
|
||||
|
||||
if nicName == "" {
|
||||
return 0, 0, "eth0", nil
|
||||
}
|
||||
|
||||
return rxBytes, txBytes, nicName, scanner.Err()
|
||||
}
|
||||
Reference in new issue
Block a user