- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
204 lines
5.0 KiB
Go
204 lines
5.0 KiB
Go
//go:build !windows
|
|
|
|
// 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
|
|
rxErrors uint64
|
|
txErrors 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
|
|
}
|
|
stat.TCPCloseWait = countCloseWait()
|
|
|
|
// 2. 采集网卡流量 + 错误包(取所有非 lo 网卡合计)
|
|
curRx, curTx, curRxErr, curTxErr, 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, rxErrors: curRxErr, txErrors: curTxErr}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 错误包:返回当前累计值(供前端判断是否激增)
|
|
stat.NetRxErrors = int64(cur.rxErrors)
|
|
stat.NetTxErrors = int64(cur.txErrors)
|
|
|
|
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, rxErrors, txErrors uint64, nicName string, err error) {
|
|
f, err := os.Open("/proc/net/dev")
|
|
if err != nil {
|
|
return 0, 0, 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) < 11 {
|
|
continue
|
|
}
|
|
|
|
// fields[0]=rx_bytes, fields[2]=rx_errors, fields[8]=tx_bytes, fields[10]=tx_errors
|
|
rb, _ := strconv.ParseUint(fields[0], 10, 64)
|
|
tb, _ := strconv.ParseUint(fields[8], 10, 64)
|
|
re, _ := strconv.ParseUint(fields[2], 10, 64)
|
|
te, _ := strconv.ParseUint(fields[10], 10, 64)
|
|
|
|
if rb > 0 || tb > 0 {
|
|
nicName = name
|
|
rxBytes += rb
|
|
txBytes += tb
|
|
rxErrors += re
|
|
txErrors += te
|
|
}
|
|
}
|
|
|
|
if nicName == "" {
|
|
return 0, 0, 0, 0, "eth0", nil
|
|
}
|
|
|
|
return rxBytes, txBytes, rxErrors, txErrors, nicName, scanner.Err()
|
|
}
|
|
|
|
// countCloseWait 从 /proc/net/tcp 统计 CLOSE_WAIT 连接数
|
|
func countCloseWait() int {
|
|
count := 0
|
|
for _, path := range []string{"/proc/net/tcp", "/proc/net/tcp6"} {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 4 && fields[3] == "08" {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
return count
|
|
}
|