Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions collector/ethtool_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions collector/ethtool_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ethtool library already implements this. Please use it.

https://pkg.go.dev/github.com/safchain/ethtool#Ethtool.GetChannels

Copy link
Contributor Author

@BarbarossaTM BarbarossaTM Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

For querying the actual values, I'm using this, but I'm not sure how I should use that for the mock? There I adopted the existing approach also used to mock the other Ethtool methods.

Do you want me to rework the mock part? If so, could you please elaborate on what you have in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, sorry, I misunderstood the use here. I didn't realize that we had a complex mock test setup for this collector.

I'm not sure how useful this is, as it's not really mocking the syscalls that the real library does. I need to look over the ethtool_linux_test.go again, but it looks like it's not really a good way to test this.

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 {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions collector/fixtures/ethtool/eth0/channels
Original file line number Diff line number Diff line change
@@ -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
Loading