package agent import ( "fmt" "log/slog" "path" "strings" "time" "github.com/henrygd/beszel/agent/deltatracker" "github.com/henrygd/beszel/internal/entities/system" psutilNet "github.com/shirou/gopsutil/v4/net" ) var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]() // NicConfig controls inclusion/exclusion of network interfaces via the NICS env var // // Behavior mirrors SensorConfig's matching logic: // - Leading '-' means blacklist mode; otherwise whitelist mode // - Supports '*' wildcards using path.Match // - In whitelist mode with an empty list, no NICs are selected // - In blacklist mode with an empty list, all NICs are selected 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 } // isValidNic determines if a NIC should be included based on NicConfig rules func isValidNic(nicName string, cfg *NicConfig) bool { // Empty list behavior differs by mode: blacklist: allow all; whitelist: allow none if len(cfg.nics) == 0 { return cfg.isBlacklist } // Exact match: return true if whitelist, false if blacklist if _, exactMatch := cfg.nics[nicName]; exactMatch { return !cfg.isBlacklist } // If no wildcards, return true if blacklist, false if whitelist if !cfg.hasWildcards { return cfg.isBlacklist } // Check for wildcard patterns 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(systemStats *system.Stats) { // network stats if len(a.netInterfaces) == 0 { // if no network interfaces, initialize again // this is a fix if agent started before network is online (#466) // maybe refactor this in the future to not cache interface names at all so we // don't miss an interface that's been added after agent started in any circumstance a.initializeNetIoStats() } if systemStats.NetworkInterfaces == nil { systemStats.NetworkInterfaces = make(map[string][4]uint64, 0) } if netIO, err := psutilNet.IOCounters(true); err == nil { msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds()) a.netIoStats.Time = time.Now() totalBytesSent := uint64(0) totalBytesRecv := uint64(0) netInterfaceDeltaTracker.Cycle() // sum all bytes sent and received for _, v := range netIO { // skip if not in valid network interfaces list if _, exists := a.netInterfaces[v.Name]; !exists { continue } totalBytesSent += v.BytesSent totalBytesRecv += v.BytesRecv // track deltas for each network interface var upDelta, downDelta uint64 upKey, downKey := fmt.Sprintf("%sup", v.Name), fmt.Sprintf("%sdown", v.Name) netInterfaceDeltaTracker.Set(upKey, v.BytesSent) netInterfaceDeltaTracker.Set(downKey, v.BytesRecv) if msElapsed > 0 { upDelta = netInterfaceDeltaTracker.Delta(upKey) * 1000 / msElapsed downDelta = netInterfaceDeltaTracker.Delta(downKey) * 1000 / msElapsed } // add interface to systemStats systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv} } // add to systemStats var bytesSentPerSecond, bytesRecvPerSecond uint64 if msElapsed > 0 { bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed } networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond)) networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond)) // add check for issue (#150) where sent is a massive number if networkSentPs > 10_000 || networkRecvPs > 10_000 { slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs) for _, v := range netIO { if _, exists := a.netInterfaces[v.Name]; !exists { continue } slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent) } // reset network I/O stats a.initializeNetIoStats() } else { systemStats.NetworkSent = networkSentPs systemStats.NetworkRecv = networkRecvPs systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond // update netIoStats a.netIoStats.BytesSent = totalBytesSent a.netIoStats.BytesRecv = totalBytesRecv } } } func (a *Agent) initializeNetIoStats() { // reset valid network interfaces a.netInterfaces = make(map[string]struct{}, 0) // parse NICS env var for whitelist / blacklist nicsEnvVal, nicsEnvExists := GetEnv("NICS") var nicCfg *NicConfig if nicsEnvExists { nicCfg = newNicConfig(nicsEnvVal) } // reset network I/O stats a.netIoStats.BytesSent = 0 a.netIoStats.BytesRecv = 0 // get intial network I/O stats if netIO, err := psutilNet.IOCounters(true); err == nil { a.netIoStats.Time = time.Now() for _, v := range netIO { if nicsEnvExists && !isValidNic(v.Name, nicCfg) { continue } if a.skipNetworkInterface(v) { continue } slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv) a.netIoStats.BytesSent += v.BytesSent a.netIoStats.BytesRecv += v.BytesRecv // store as a valid network interface a.netInterfaces[v.Name] = struct{}{} } } } func (a *Agent) skipNetworkInterface(v psutilNet.IOCountersStat) bool { 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"), v.BytesRecv == 0, v.BytesSent == 0: return true default: return false } }