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