104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
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
|
|
}
|