From f23a956c4fece3bd454925a40e598bab44f4fd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Wed, 7 Jul 2021 11:05:06 +0200 Subject: [PATCH] collector/netdev_linux.go: Use netlink to get stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of parsing `/proc/net/dev` to get network interface statistics, get them from a netlink call. Internally, both come from the [rtnl_link_stats64] struct, but with `/proc/net/dev`, some of the values are aggregated together in [dev_seq_printf_stats], so we get less information out of them. This commit maintains compatibility by aggregating those stats back into the same metrics. [rtnl_link_stats64]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_link.h#L42-L246 [dev_seq_printf_stats]: https://github.com/torvalds/linux/blob/master/net/core/net-procfs.c#L75-L97 Signed-off-by: Benoît Knecht --- collector/netdev_linux.go | 100 +++++++++---------------- collector/netdev_linux_test.go | 133 +++++++++++++++++++++++++++------ 2 files changed, 149 insertions(+), 84 deletions(-) diff --git a/collector/netdev_linux.go b/collector/netdev_linux.go index baf7f4eb..5a0d5c34 100644 --- a/collector/netdev_linux.go +++ b/collector/netdev_linux.go @@ -17,86 +17,60 @@ package collector import ( - "bufio" - "fmt" - "io" - "os" - "regexp" - "strconv" - "strings" - "github.com/go-kit/log" "github.com/go-kit/log/level" -) -var ( - procNetDevInterfaceRE = regexp.MustCompile(`^(.+): *(.+)$`) - procNetDevFieldSep = regexp.MustCompile(` +`) + "github.com/jsimonetti/rtnetlink" ) func getNetDevStats(filter *deviceFilter, logger log.Logger) (netDevStats, error) { - file, err := os.Open(procFilePath("net/dev")) + conn, err := rtnetlink.Dial(nil) if err != nil { return nil, err } - defer file.Close() + defer conn.Close() - return parseNetDevStats(file, filter, logger) -} - -func parseNetDevStats(r io.Reader, filter *deviceFilter, logger log.Logger) (netDevStats, error) { - scanner := bufio.NewScanner(r) - scanner.Scan() // skip first header - scanner.Scan() - parts := strings.Split(scanner.Text(), "|") - if len(parts) != 3 { // interface + receive + transmit - return nil, fmt.Errorf("invalid header line in net/dev: %s", - scanner.Text()) + links, err := conn.Link.List() + if err != nil { + return nil, err } - receiveHeader := strings.Fields(parts[1]) - transmitHeader := strings.Fields(parts[2]) - headerLength := len(receiveHeader) + len(transmitHeader) + return netlinkStats(links, filter, logger), nil +} - netDev := netDevStats{} - for scanner.Scan() { - line := strings.TrimLeft(scanner.Text(), " ") - parts := procNetDevInterfaceRE.FindStringSubmatch(line) - if len(parts) != 3 { - return nil, fmt.Errorf("couldn't get interface name, invalid line in net/dev: %q", line) - } +func netlinkStats(links []rtnetlink.LinkMessage, filter *deviceFilter, logger log.Logger) netDevStats { + metrics := netDevStats{} - dev := parts[1] - if filter.ignored(dev) { - level.Debug(logger).Log("msg", "Ignoring device", "device", dev) + for _, msg := range links { + name := msg.Attributes.Name + stats := msg.Attributes.Stats64 + + if filter.ignored(name) { + level.Debug(logger).Log("msg", "Ignoring device", "device", name) continue } - values := procNetDevFieldSep.Split(strings.TrimLeft(parts[2], " "), -1) - if len(values) != headerLength { - return nil, fmt.Errorf("couldn't get values, invalid line in net/dev: %q", parts[2]) + // https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_link.h#L42-L246 + // https://github.com/torvalds/linux/blob/master/net/core/net-procfs.c#L75-L97 + metrics[name] = map[string]uint64{ + "receive_packets": stats.RXPackets, + "transmit_packets": stats.TXPackets, + "receive_bytes": stats.RXBytes, + "transmit_bytes": stats.TXBytes, + "receive_errs": stats.RXErrors, + "transmit_errs": stats.TXErrors, + "receive_drop": stats.RXDropped + stats.RXMissedErrors, + "transmit_drop": stats.TXDropped, + "receive_multicast": stats.Multicast, + "transmit_colls": stats.Collisions, + "receive_frame": stats.RXLengthErrors + stats.RXOverErrors + stats.RXCRCErrors + stats.RXFrameErrors, + "receive_fifo": stats.RXFIFOErrors, + "transmit_carrier": stats.TXAbortedErrors + stats.TXCarrierErrors + stats.TXHeartbeatErrors + stats.TXWindowErrors, + "transmit_fifo": stats.TXFIFOErrors, + "receive_compressed": stats.RXCompressed, + "transmit_compressed": stats.TXCompressed, } - - devStats := map[string]uint64{} - addStats := func(key, value string) { - v, err := strconv.ParseUint(value, 0, 64) - if err != nil { - level.Debug(logger).Log("msg", "invalid value in netstats", "key", key, "value", value, "err", err) - return - } - - devStats[key] = v - } - - for i := 0; i < len(receiveHeader); i++ { - addStats("receive_"+receiveHeader[i], values[i]) - } - - for i := 0; i < len(transmitHeader); i++ { - addStats("transmit_"+transmitHeader[i], values[i+len(receiveHeader)]) - } - - netDev[dev] = devStats } - return netDev, scanner.Err() + + return metrics } diff --git a/collector/netdev_linux_test.go b/collector/netdev_linux_test.go index 14935df6..8dae9aa3 100644 --- a/collector/netdev_linux_test.go +++ b/collector/netdev_linux_test.go @@ -14,25 +14,125 @@ package collector import ( - "os" "testing" "github.com/go-kit/log" + + "github.com/jsimonetti/rtnetlink" ) -func TestNetDevStatsIgnore(t *testing.T) { - file, err := os.Open("fixtures/proc/net/dev") - if err != nil { - t.Fatal(err) - } - defer file.Close() +var links = []rtnetlink.LinkMessage{ + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "tun0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 24, + TXPackets: 934, + RXBytes: 1888, + TXBytes: 67120, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "veth4B09XN", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 8, + TXPackets: 10640, + RXBytes: 648, + TXBytes: 1943284, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "lo", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 1832522, + TXPackets: 1832522, + RXBytes: 435303245, + TXBytes: 435303245, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "eth0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 520993275, + TXPackets: 43451486, + RXBytes: 68210035552, + TXBytes: 9315587528, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "lxcbr0", + Stats64: &rtnetlink.LinkStats64{ + TXPackets: 28339, + TXBytes: 2630299, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "wlan0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 13899359, + TXPackets: 11726200, + RXBytes: 10437182923, + TXBytes: 2851649360, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "docker0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 1065585, + TXPackets: 1929779, + RXBytes: 64910168, + TXBytes: 2681662018, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "ibr10:30", + Stats64: &rtnetlink.LinkStats64{}, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "flannel.1", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 228499337, + TXPackets: 258369223, + RXBytes: 18144009813, + TXBytes: 20758990068, + TXDropped: 64, + }, + }, + }, + { + Attributes: &rtnetlink.LinkAttributes{ + Name: "💩0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 105557, + TXPackets: 304261, + RXBytes: 57750104, + TXBytes: 404570255, + Multicast: 72, + }, + }, + }, +} +func TestNetDevStatsIgnore(t *testing.T) { filter := newDeviceFilter("^veth", "") - netStats, err := parseNetDevStats(file, &filter, log.NewNopLogger()) - if err != nil { - t.Fatal(err) - } + netStats := netlinkStats(links, &filter, log.NewNopLogger()) if want, got := uint64(10437182923), netStats["wlan0"]["receive_bytes"]; want != got { t.Errorf("want netstat wlan0 bytes %v, got %v", want, got) @@ -64,17 +164,8 @@ func TestNetDevStatsIgnore(t *testing.T) { } func TestNetDevStatsAccept(t *testing.T) { - file, err := os.Open("fixtures/proc/net/dev") - if err != nil { - t.Fatal(err) - } - defer file.Close() - filter := newDeviceFilter("", "^💩0$") - netStats, err := parseNetDevStats(file, &filter, log.NewNopLogger()) - if err != nil { - t.Fatal(err) - } + netStats := netlinkStats(links, &filter, log.NewNopLogger()) if want, got := 1, len(netStats); want != got { t.Errorf("want count of devices to be %d, got %d", want, got)