package main import ( "context" "os/exec" "path/filepath" "strconv" "strings" "time" ) func counterDelta(current, previous uint64) uint64 { if current < previous { return 0 } return current - previous } func updateInterfaceDeltas(v *NetworkInterface, old NetworkInterface, elapsed float64) { if elapsed <= 0 { return } v.ReceivePerSec = float64(counterDelta(v.ReceiveBytes, old.ReceiveBytes)) / elapsed v.TransmitPerSec = float64(counterDelta(v.TransmitBytes, old.TransmitBytes)) / elapsed v.NewErrors = counterDelta(v.ReceiveErrors, old.ReceiveErrors) + counterDelta(v.TransmitErrors, old.TransmitErrors) v.NewReceiveDrops = counterDelta(v.ReceiveDrops, old.ReceiveDrops) v.NewTransmitDrops = counterDelta(v.TransmitDrops, old.TransmitDrops) v.NewDrops = v.NewReceiveDrops + v.NewTransmitDrops for name, value := range v.FaultCounters { if previous, ok := old.FaultCounters[name]; ok { v.NewFaults += counterDelta(value, previous) } } // A transition matters even if the interface has not carried traffic yet. v.LinkDown = old.Carrier && !v.Carrier } func interfaceLinkDown(v NetworkInterface) bool { if v.Kind == "Firewall bridge" || v.Kind == "Firewall link" { return false } if v.LinkDown { return true } used := v.Master != "" || v.ReceiveBytes+v.TransmitBytes > 0 if v.Kind == "Physical" { return used && (!v.Carrier || v.State == "down") } return used && v.State == "down" } func collectInterfaceFaults(v *NetworkInterface) { base := filepath.Join("/sys/class/net", v.Name) // /proc/net/dev folds missed packets into RX drop. Sysfs keeps software // drops separate from rx_missed_errors, which must still trigger an alert. if n, err := strconv.ParseUint(readTrimmed(filepath.Join(base, "statistics/rx_dropped")), 10, 64); err == nil { v.ReceiveDrops = n } v.FaultCounters = map[string]uint64{} for _, name := range []string{"rx_crc_errors", "rx_frame_errors", "rx_fifo_errors", "rx_missed_errors", "tx_fifo_errors", "tx_carrier_errors", "carrier_down_count"} { path := filepath.Join(base, "statistics", name) if name == "carrier_down_count" { path = filepath.Join(base, name) } if n, err := strconv.ParseUint(readTrimmed(path), 10, 64); err == nil { v.FaultCounters[name] = n } } // Only Atlantic exposes the driver-specific names below. No configuration // changes, and a missing ethtool is not itself a network failure. driver, err := filepath.EvalSymlinks(filepath.Join(base, "device/driver")) if err != nil || filepath.Base(driver) != "atlantic" { return } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if out, err := exec.CommandContext(ctx, "ethtool", "-S", v.Name).Output(); err == nil { for k, n := range parseAtlanticFaults(string(out)) { v.FaultCounters["atlantic:"+k] = n } } } func parseAtlanticFaults(output string) map[string]uint64 { result := map[string]uint64{} for _, line := range strings.Split(output, "\n") { key, value, ok := strings.Cut(strings.TrimSpace(line), ":") if !ok { continue } fields := strings.Fields(key) if len(fields) == 0 { continue } switch fields[len(fields)-1] { case "InErrors", "InDroppedDma", "AllocFails", "SkbAllocFails", "Restarts": if n, err := strconv.ParseUint(strings.TrimSpace(value), 10, 64); err == nil { result[key] = n } } } return result }