From 6084d55883c509153b41594fef839ea27d7071ba Mon Sep 17 00:00:00 2001 From: Maximilian Wilhelm Date: Tue, 6 Jan 2026 17:28:35 +0100 Subject: [PATCH] collector/ethtool: Expose NIC channel configuration When running a large fleet of machines running network-heavy loads and with different hardware configurations, it is important to get an overview about their configuration. Ethtool exporter already exposes a lot of useful info like port configuration, driver, and obviously stats, however did not expose the channel configuration. This commit adds two new metrics exposing the current and maximum supported channel configuration, with each metric exposing one instance for combined, other, rx, and tx. Signed-off-by: Maximilian Wilhelm --- collector/ethtool_linux.go | 49 +++++++++++++ collector/ethtool_linux_test.go | 87 ++++++++++++++++++++++++ collector/fixtures/ethtool/eth0/channels | 13 ++++ 3 files changed, 149 insertions(+) create mode 100644 collector/fixtures/ethtool/eth0/channels diff --git a/collector/ethtool_linux.go b/collector/ethtool_linux.go index e4d86bcd8c..9d80fb13b4 100644 --- a/collector/ethtool_linux.go +++ b/collector/ethtool_linux.go @@ -49,6 +49,7 @@ type Ethtool interface { DriverInfo(string) (ethtool.DrvInfo, error) Stats(string) (map[string]uint64, error) LinkInfo(string) (ethtool.EthtoolCmd, error) + GetChannels(string) (ethtool.Channels, error) } type ethtoolLibrary struct { @@ -69,6 +70,10 @@ func (e *ethtoolLibrary) LinkInfo(intf string) (ethtool.EthtoolCmd, error) { return ethtoolCmd, err } +func (e *ethtoolLibrary) GetChannels(intf string) (ethtool.Channels, error) { + return e.ethtool.GetChannels(intf) +} + type ethtoolCollector struct { fs sysfs.FS entries map[string]*prometheus.Desc @@ -199,6 +204,21 @@ func makeEthtoolCollector(logger *slog.Logger) (*ethtoolCollector, error) { "If this port is using autonegotiate", []string{"device"}, nil, ), + + // channel info + // + // Each metric will have four instances differentiated by its type, + // with type being one of rx, tx, other, or combined. + "channels_max": prometheus.NewDesc( + prometheus.BuildFQName(namespace, "ethtool", "channels_max"), + "Maximum supported network interface channels", + []string{"device", "type"}, nil, + ), + "channels_current": prometheus.NewDesc( + prometheus.BuildFQName(namespace, "ethtool", "channels_current"), + "Currently configured network interface channels", + []string{"device", "type"}, nil, + ), }, infoDesc: prometheus.NewDesc( prometheus.BuildFQName(namespace, "ethtool", "info"), @@ -370,6 +390,33 @@ func (c *ethtoolCollector) updateSpeeds(ch chan<- prometheus.Metric, prefix stri } } +func (c *ethtoolCollector) updateChannels(ch chan<- prometheus.Metric, device string) { + channels, err := c.ethtool.GetChannels(device) + if err != nil { + if errno, ok := err.(syscall.Errno); ok { + if err == unix.EOPNOTSUPP { + c.logger.Debug("ethtool driver info error", "err", err, "device", device, "errno", uint(errno)) + } else if errno != 0 { + c.logger.Error("ethtool get channels error", "err", err, "device", device, "errno", uint(errno)) + } + } else { + c.logger.Error("ethtool get channels error", "err", err, "device", device) + } + + return + } + + ch <- prometheus.MustNewConstMetric(c.entry("channels_max"), prometheus.GaugeValue, float64(channels.MaxRx), device, "rx") + ch <- prometheus.MustNewConstMetric(c.entry("channels_max"), prometheus.GaugeValue, float64(channels.MaxTx), device, "tx") + ch <- prometheus.MustNewConstMetric(c.entry("channels_max"), prometheus.GaugeValue, float64(channels.MaxOther), device, "other") + ch <- prometheus.MustNewConstMetric(c.entry("channels_max"), prometheus.GaugeValue, float64(channels.MaxCombined), device, "combined") + + ch <- prometheus.MustNewConstMetric(c.entry("channels_current"), prometheus.GaugeValue, float64(channels.RxCount), device, "rx") + ch <- prometheus.MustNewConstMetric(c.entry("channels_current"), prometheus.GaugeValue, float64(channels.TxCount), device, "tx") + ch <- prometheus.MustNewConstMetric(c.entry("channels_current"), prometheus.GaugeValue, float64(channels.OtherCount), device, "other") + ch <- prometheus.MustNewConstMetric(c.entry("channels_current"), prometheus.GaugeValue, float64(channels.CombinedCount), device, "combined") +} + func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error { netClass, err := c.fs.NetClassDevices() if err != nil { @@ -429,6 +476,8 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error { } } + c.updateChannels(ch, device) + stats, err = c.ethtool.Stats(device) // If Stats() returns EOPNOTSUPP it doesn't support ethtool stats. Log that only at Debug level. diff --git a/collector/ethtool_linux_test.go b/collector/ethtool_linux_test.go index 84cca88897..a7980117c2 100644 --- a/collector/ethtool_linux_test.go +++ b/collector/ethtool_linux_test.go @@ -257,6 +257,81 @@ func (e *EthtoolFixture) LinkInfo(intf string) (ethtool.EthtoolCmd, error) { return res, err } +func (e *EthtoolFixture) GetChannels(intf string) (ethtool.Channels, error) { + res := ethtool.Channels{} + + fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf, "channels")) + if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { + // The fixture for this interface doesn't exist. Translate that to unix.EOPNOTSUPP + // to replicate an interface that doesn't support ethtool driver info + return res, unix.EOPNOTSUPP + } + if err != nil { + return res, err + } + defer fixtureFile.Close() + + scanner := bufio.NewScanner(fixtureFile) + currentConfig := false + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "Channel parameters for") || strings.HasPrefix(line, "Pre-set maximums") { + continue + } + + line = strings.Trim(line, " ") + items := strings.Split(line, ":") + fmt.Printf("line: %s\n", line) + fmt.Printf("items: %s\n", strings.Join(items, ",")) + switch items[0] { + case "Current hardware settings": + currentConfig = true + case "RX": + if currentConfig { + res.RxCount = readChannel(items[1]) + } else { + res.MaxRx = readChannel(items[1]) + } + case "TX": + if currentConfig { + res.TxCount = readChannel(items[1]) + } else { + res.MaxTx = readChannel(items[1]) + } + case "Other": + if currentConfig { + res.OtherCount = readChannel(items[1]) + } else { + res.MaxOther = readChannel(items[1]) + } + case "Combined": + if currentConfig { + res.CombinedCount = readChannel(items[1]) + } else { + res.MaxCombined = readChannel(items[1]) + } + } + } + + return res, nil +} + +func readChannel(val string) uint32 { + val = strings.TrimSpace(val) + if val == "n/a" { + return 0 + } + + intVal, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return 0 + } + + return uint32(intVal) + +} + func NewEthtoolTestCollector(logger *slog.Logger) (Collector, error) { collector, err := makeEthtoolCollector(logger) if err != nil { @@ -291,6 +366,18 @@ func TestEthToolCollector(t *testing.T) { testcase := `# HELP node_ethtool_align_errors Network interface align_errors # TYPE node_ethtool_align_errors untyped node_ethtool_align_errors{device="eth0"} 0 +# HELP node_ethtool_channels_current Currently configured network interface channels +# TYPE node_ethtool_channels_current gauge +node_ethtool_channels_current{device="eth0",type="combined"} 128 +node_ethtool_channels_current{device="eth0",type="other"} 1 +node_ethtool_channels_current{device="eth0",type="rx"} 0 +node_ethtool_channels_current{device="eth0",type="tx"} 0 +# HELP node_ethtool_channels_max Maximum supported network interface channels +# TYPE node_ethtool_channels_max gauge +node_ethtool_channels_max{device="eth0",type="combined"} 252 +node_ethtool_channels_max{device="eth0",type="other"} 1 +node_ethtool_channels_max{device="eth0",type="rx"} 252 +node_ethtool_channels_max{device="eth0",type="tx"} 252 # HELP node_ethtool_info A metric with a constant '1' value labeled by bus_info, device, driver, expansion_rom_version, firmware_version, version. # TYPE node_ethtool_info gauge node_ethtool_info{bus_info="0000:00:1f.6",device="eth0",driver="e1000e",expansion_rom_version="",firmware_version="0.5-4",version="5.11.0-22-generic"} 1 diff --git a/collector/fixtures/ethtool/eth0/channels b/collector/fixtures/ethtool/eth0/channels new file mode 100644 index 0000000000..1524608aee --- /dev/null +++ b/collector/fixtures/ethtool/eth0/channels @@ -0,0 +1,13 @@ +# ethtool --show-channels eth0 +Channel parameters for eth0: +Pre-set maximums: +RX: 252 +TX: 252 +Other: 1 +Combined: 252 +Current hardware settings: +# Testing n/a (which will be translated into 0) +RX: n/a +TX: 0 +Other: 1 +Combined: 128