| package agent
|
|
|
| import (
|
| "fmt"
|
| "log/slog"
|
| "path"
|
| "strings"
|
| "time"
|
|
|
| "github.com/henrygd/beszel/agent/deltatracker"
|
| "github.com/henrygd/beszel/agent/utils"
|
| "github.com/henrygd/beszel/internal/entities/system"
|
| psutilNet "github.com/shirou/gopsutil/v4/net"
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| type NicConfig struct {
|
| nics map[string]struct{}
|
| isBlacklist bool
|
| hasWildcards bool
|
| }
|
|
|
| func newNicConfig(nicsEnvVal string) *NicConfig {
|
| cfg := &NicConfig{
|
| nics: make(map[string]struct{}),
|
| }
|
| if strings.HasPrefix(nicsEnvVal, "-") {
|
| cfg.isBlacklist = true
|
| nicsEnvVal = nicsEnvVal[1:]
|
| }
|
| for nic := range strings.SplitSeq(nicsEnvVal, ",") {
|
| nic = strings.TrimSpace(nic)
|
| if nic != "" {
|
| cfg.nics[nic] = struct{}{}
|
| if strings.Contains(nic, "*") {
|
| cfg.hasWildcards = true
|
| }
|
| }
|
| }
|
| return cfg
|
| }
|
|
|
|
|
| func isValidNic(nicName string, cfg *NicConfig) bool {
|
|
|
| if len(cfg.nics) == 0 {
|
| return cfg.isBlacklist
|
| }
|
|
|
|
|
| if _, exactMatch := cfg.nics[nicName]; exactMatch {
|
| return !cfg.isBlacklist
|
| }
|
|
|
|
|
| if !cfg.hasWildcards {
|
| return cfg.isBlacklist
|
| }
|
|
|
|
|
| for pattern := range cfg.nics {
|
| if !strings.Contains(pattern, "*") {
|
| continue
|
| }
|
| if match, _ := path.Match(pattern, nicName); match {
|
| return !cfg.isBlacklist
|
| }
|
| }
|
|
|
| return cfg.isBlacklist
|
| }
|
|
|
| func (a *Agent) updateNetworkStats(cacheTimeMs uint16, systemStats *system.Stats) {
|
|
|
| a.ensureNetInterfacesInitialized()
|
|
|
| a.ensureNetworkInterfacesMap(systemStats)
|
|
|
| if netIO, err := psutilNet.IOCounters(true); err == nil {
|
| nis, msElapsed := a.loadAndTickNetBaseline(cacheTimeMs)
|
| totalBytesSent, totalBytesRecv := a.sumAndTrackPerNicDeltas(cacheTimeMs, msElapsed, netIO, systemStats)
|
| bytesSentPerSecond, bytesRecvPerSecond := a.computeBytesPerSecond(msElapsed, totalBytesSent, totalBytesRecv, nis)
|
| a.applyNetworkTotals(cacheTimeMs, netIO, systemStats, nis, totalBytesSent, totalBytesRecv, bytesSentPerSecond, bytesRecvPerSecond)
|
| }
|
| }
|
|
|
| func (a *Agent) initializeNetIoStats() {
|
|
|
| a.netInterfaces = make(map[string]struct{}, 0)
|
|
|
|
|
| nicsEnvVal, nicsEnvExists := utils.GetEnv("NICS")
|
| var nicCfg *NicConfig
|
| if nicsEnvExists {
|
| nicCfg = newNicConfig(nicsEnvVal)
|
| }
|
|
|
|
|
| if netIO, err := psutilNet.IOCounters(true); err == nil {
|
| for _, v := range netIO {
|
| if skipNetworkInterface(v, nicCfg) {
|
| continue
|
| }
|
| slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
|
|
| a.netInterfaces[v.Name] = struct{}{}
|
| }
|
| }
|
|
|
|
|
| a.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])
|
| a.netIoStats = make(map[uint16]system.NetIoStats)
|
| }
|
|
|
|
|
| func (a *Agent) ensureNetInterfacesInitialized() {
|
| if len(a.netInterfaces) == 0 {
|
|
|
|
|
|
|
|
|
| a.initializeNetIoStats()
|
| }
|
| }
|
|
|
|
|
| func (a *Agent) ensureNetworkInterfacesMap(systemStats *system.Stats) {
|
| if systemStats.NetworkInterfaces == nil {
|
| systemStats.NetworkInterfaces = make(map[string][4]uint64, 0)
|
| }
|
| }
|
|
|
|
|
| func (a *Agent) loadAndTickNetBaseline(cacheTimeMs uint16) (netIoStat system.NetIoStats, msElapsed uint64) {
|
| netIoStat = a.netIoStats[cacheTimeMs]
|
| if netIoStat.Time.IsZero() {
|
| netIoStat.Time = time.Now()
|
| msElapsed = 0
|
| } else {
|
| msElapsed = uint64(time.Since(netIoStat.Time).Milliseconds())
|
| netIoStat.Time = time.Now()
|
| }
|
| return netIoStat, msElapsed
|
| }
|
|
|
|
|
| func (a *Agent) sumAndTrackPerNicDeltas(cacheTimeMs uint16, msElapsed uint64, netIO []psutilNet.IOCountersStat, systemStats *system.Stats) (totalBytesSent, totalBytesRecv uint64) {
|
| tracker := a.netInterfaceDeltaTrackers[cacheTimeMs]
|
| if tracker == nil {
|
| tracker = deltatracker.NewDeltaTracker[string, uint64]()
|
| a.netInterfaceDeltaTrackers[cacheTimeMs] = tracker
|
| }
|
| tracker.Cycle()
|
|
|
| for _, v := range netIO {
|
| if _, exists := a.netInterfaces[v.Name]; !exists {
|
| continue
|
| }
|
| totalBytesSent += v.BytesSent
|
| totalBytesRecv += v.BytesRecv
|
|
|
| var upDelta, downDelta uint64
|
| upKey, downKey := fmt.Sprintf("%sup", v.Name), fmt.Sprintf("%sdown", v.Name)
|
| tracker.Set(upKey, v.BytesSent)
|
| tracker.Set(downKey, v.BytesRecv)
|
| if msElapsed > 0 {
|
| if prevVal, ok := tracker.Previous(upKey); ok {
|
| var deltaBytes uint64
|
| if v.BytesSent >= prevVal {
|
| deltaBytes = v.BytesSent - prevVal
|
| } else {
|
| deltaBytes = v.BytesSent
|
| }
|
| upDelta = deltaBytes * 1000 / msElapsed
|
| }
|
| if prevVal, ok := tracker.Previous(downKey); ok {
|
| var deltaBytes uint64
|
| if v.BytesRecv >= prevVal {
|
| deltaBytes = v.BytesRecv - prevVal
|
| } else {
|
| deltaBytes = v.BytesRecv
|
| }
|
| downDelta = deltaBytes * 1000 / msElapsed
|
| }
|
| }
|
| systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}
|
| }
|
|
|
| return totalBytesSent, totalBytesRecv
|
| }
|
|
|
|
|
| func (a *Agent) computeBytesPerSecond(msElapsed, totalBytesSent, totalBytesRecv uint64, nis system.NetIoStats) (bytesSentPerSecond, bytesRecvPerSecond uint64) {
|
| if msElapsed > 0 {
|
| bytesSentPerSecond = (totalBytesSent - nis.BytesSent) * 1000 / msElapsed
|
| bytesRecvPerSecond = (totalBytesRecv - nis.BytesRecv) * 1000 / msElapsed
|
| }
|
| return bytesSentPerSecond, bytesRecvPerSecond
|
| }
|
|
|
|
|
| func (a *Agent) applyNetworkTotals(
|
| cacheTimeMs uint16,
|
| netIO []psutilNet.IOCountersStat,
|
| systemStats *system.Stats,
|
| nis system.NetIoStats,
|
| totalBytesSent, totalBytesRecv uint64,
|
| bytesSentPerSecond, bytesRecvPerSecond uint64,
|
| ) {
|
| if bytesSentPerSecond > 10_000_000_000 || bytesRecvPerSecond > 10_000_000_000 {
|
| slog.Warn("Invalid net stats. Resetting.", "sent", bytesSentPerSecond, "recv", bytesRecvPerSecond)
|
| for _, v := range netIO {
|
| if _, exists := a.netInterfaces[v.Name]; !exists {
|
| continue
|
| }
|
| slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
|
| }
|
| a.initializeNetIoStats()
|
| delete(a.netIoStats, cacheTimeMs)
|
| delete(a.netInterfaceDeltaTrackers, cacheTimeMs)
|
| systemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0
|
| return
|
| }
|
|
|
| systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
| nis.BytesSent = totalBytesSent
|
| nis.BytesRecv = totalBytesRecv
|
| a.netIoStats[cacheTimeMs] = nis
|
| }
|
|
|
|
|
| func skipNetworkInterface(v psutilNet.IOCountersStat, nicCfg *NicConfig) bool {
|
| if nicCfg != nil {
|
| if !isValidNic(v.Name, nicCfg) {
|
| return true
|
| }
|
|
|
| if !nicCfg.isBlacklist {
|
| return false
|
| }
|
|
|
| }
|
|
|
| switch {
|
| case strings.HasPrefix(v.Name, "lo"),
|
| strings.HasPrefix(v.Name, "docker"),
|
| strings.HasPrefix(v.Name, "br-"),
|
| strings.HasPrefix(v.Name, "veth"),
|
| strings.HasPrefix(v.Name, "bond"),
|
| strings.HasPrefix(v.Name, "cali"),
|
| v.BytesRecv == 0,
|
| v.BytesSent == 0:
|
| return true
|
| default:
|
| return false
|
| }
|
| }
|
|
|