feat: 重构监控中心导航,新增资源监控4大模块

- 主菜单「监控告警」→「监控中心」
- 「告警中心」→「告警服务」移至链路追踪下方
- 新增资源监控 Tab 页(主机/中间件/SNMP/站点拨测)
- Agent 新增网络采集器(network)和磁盘IO采集器(diskio)
- 后端扩展 Agent 上报模型,支持 NetworkStat/DiskIOStat
- 前端 ServerTableRow 展示网络和磁盘IO实时指标
- WebSocket MetricsUpdate 推送扩展
This commit is contained in:
attacker committed 2026-06-18 09:30:45 +00:00
1 parent 020b7a9063
commit f3a9e5e8d0
21 files changed
+1007 -32

No files matched your search

+121
View File
@@ -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()
}
+171
View File
@@ -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()
}
+1 -1
View File
@@ -1,5 +1,5 @@
module ezops/agent
go 1.24
go 1.22
require golang.org/x/sys v0.28.0
+19 -2
View File
@@ -64,6 +64,8 @@ func main() {
memCollector := collector.NewMemCollector()
diskCollector := collector.NewDiskCollector()
portCollector := collector.NewPortCollector()
netCollector := collector.NewNetworkCollector()
diskIOCollector := collector.NewDiskIOCollector()
rep := reporter.New(*server, *token, logger)
// 初始化任务执行器与任务客户端(与 reporter 复用相同的 server/token/hostname)
@@ -108,12 +110,27 @@ func main() {
report.Ports = ports
}
if netStat, err := netCollector.Collect(); err != nil {
logger.Printf("网络采集异常: %v", err)
} else {
report.Network = netStat
}
if dioStat, err := diskIOCollector.Collect(); err != nil {
logger.Printf("磁盘IO采集异常: %v", err)
} else {
report.DiskIO = dioStat
}
if err := rep.Send(ctx, report); err != nil {
logger.Printf("上报失败: %v", err)
return
}
logger.Printf("上报成功: cpu=%.2f%% mem=%.2f%% disks=%d ports=%d",
report.CPU.UsagePercent, report.Memory.UsagePercent, len(report.Disks), len(report.Ports))
logger.Printf("上报成功: cpu=%.2f%% mem=%.2f%% disks=%d ports=%d net=%dconn/%dwait netIO=%.0f/%.0fB/s diskIO=%.0f/%.0fB/s",
report.CPU.UsagePercent, report.Memory.UsagePercent, len(report.Disks), len(report.Ports),
report.Network.TCPEstablished, report.Network.TCPTimeWait,
report.Network.NetRxBPS, report.Network.NetTxBPS,
report.DiskIO.ReadBPS, report.DiskIO.WriteBPS)
}
// pollAndExecute 执行单次任务轮询:拉取待执行任务 → 逐个执行 → 回传结果。
+8 -6
View File
@@ -6,12 +6,14 @@ package model
// Report 为单次采集周期的完整上报报文
type Report struct {
Hostname string `json:"hostname"` // 主机名
Timestamp string `json:"timestamp"` // RFC3339 格式的 UTC 时间戳
CPU CPUStat `json:"cpu"` // CPU 指标
Memory MemoryStat `json:"memory"` // 内存指标
Disks []DiskStat `json:"disks"` // 磁盘指标列表
Ports []PortStat `json:"ports"` // 监听端口列表
Hostname string `json:"hostname"` // 主机名
Timestamp string `json:"timestamp"` // RFC3339 格式的 UTC 时间戳
CPU CPUStat `json:"cpu"` // CPU 指标
Memory MemoryStat `json:"memory"` // 内存指标
Disks []DiskStat `json:"disks"` // 磁盘指标列表
Ports []PortStat `json:"ports"` // 监听端口列表
Network NetworkStat `json:"network"` // 网络指标
DiskIO DiskIOStat `json:"disk_io"` // 磁盘 IO 指标
}
// CPUStat CPU 相关指标
+28
View File
@@ -0,0 +1,28 @@
package model
// NetworkStat 网络相关指标
type NetworkStat struct {
TCPEstablished int `json:"tcp_established"` // ESTABLISHED 连接数
TCPTimeWait int `json:"tcp_time_wait"` // TIME_WAIT 连接数
TCPTotal int `json:"tcp_total"` // TCP 总连接数
NicName string `json:"nic_name"` // 主网卡名
NetRxBPS float64 `json:"net_rx_bps"` // 网卡接收速率 (bytes/s)
NetTxBPS float64 `json:"net_tx_bps"` // 网卡发送速率 (bytes/s)
}
// DiskIOStat 磁盘 IO 相关指标
type DiskIOStat struct {
ReadBPS float64 `json:"read_bps"` // 读取速率 (bytes/s)
WriteBPS float64 `json:"write_bps"` // 写入速率 (bytes/s)
ReadIOPS float64 `json:"read_iops"` // 读取 IOPS (count/s)
WriteIOPS float64 `json:"write_iops"` // 写入 IOPS (count/s)
}
// SystemStat 系统信息(启动时采集一次)
type SystemStat struct {
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
Kernel string `json:"kernel"` // 内核版本
Uptime int64 `json:"uptime"` // 运行秒数
UlimitNofile int64 `json:"ulimit_nofile"` // ulimit -n (最大文件描述符)
UlimitNproc int64 `json:"ulimit_nproc"` // ulimit -u (最大进程数)
}