From 596ff45f8ff8da205a236d0d1d04b6f6304d0078 Mon Sep 17 00:00:00 2001 From: "W. Andrew Denton" Date: Thu, 29 Apr 2021 11:05:29 -0700 Subject: [PATCH 1/5] ethtool: Add a new ethtool stats collector (metrics equivalent to "ethtool -S") Signed-off-by: W. Andrew Denton --- collector/ethtool_linux.go | 237 ++++++++++++++++++++++++++++++ collector/ethtool_linux_test.go | 70 +++++++++ collector/fixtures/e2e-output.txt | 40 +++++ collector/fixtures/ethtool/eth0 | 15 ++ end-to-end-test.sh | 2 + go.mod | 1 + go.sum | 2 + 7 files changed, 367 insertions(+) create mode 100644 collector/ethtool_linux.go create mode 100644 collector/ethtool_linux_test.go create mode 100644 collector/fixtures/ethtool/eth0 diff --git a/collector/ethtool_linux.go b/collector/ethtool_linux.go new file mode 100644 index 00000000..912e31e5 --- /dev/null +++ b/collector/ethtool_linux.go @@ -0,0 +1,237 @@ +// Copyright 2021 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !noethtool + +// The hard work of collecting data from the kernel via the ethtool interfaces is done by +// https://github.com/safchain/ethtool/ +// by Sylvain Afchain. Used under the Apache license. + +package collector + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "syscall" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/procfs/sysfs" + "github.com/safchain/ethtool" + "gopkg.in/alecthomas/kingpin.v2" +) + +var ( + receivedRegex = regexp.MustCompile(`_rx_`) + transmittedRegex = regexp.MustCompile(`_tx_`) + ethtoolFixtures = kingpin.Flag("collector.ethtool.fixtures", "test fixtures to use for ethtool collector end-to-end testing").Default("").String() +) + +type EthtoolStats interface { + Stats(string) (map[string]uint64, error) +} + +type ethtoolStats struct { +} + +func (e *ethtoolStats) Stats(intf string) (map[string]uint64, error) { + return ethtool.Stats(intf) +} + +type ethtoolCollector struct { + fs sysfs.FS + entries map[string]*prometheus.Desc + logger log.Logger + stats EthtoolStats +} + +type EthtoolFixture struct { + fixturePath string +} + +func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { + res := make(map[string]uint64) + + fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf)) + if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { + // The fixture for this interface doesn't exist. That's OK because it replicates + // an interface that doesn't support ethtool. + return res, nil + } + if err != nil { + return res, err + } + defer fixtureFile.Close() + + scanner := bufio.NewScanner(fixtureFile) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "NIC statistics:") { + continue + } + line = strings.Trim(line, " ") + items := strings.Split(line, ": ") + val, err := strconv.ParseUint(items[1], 10, 64) + if err != nil { + return res, err + } + res[items[0]] = val + } + + return res, err +} + +func NewEthtoolTestCollector(logger log.Logger) (Collector, error) { + collector, err := makeEthtoolCollector(logger) + collector.stats = &EthtoolFixture{ + fixturePath: *ethtoolFixtures, + } + if err != nil { + return nil, err + } + return collector, nil +} + +// makeEthtoolCollector is the internal constructor for EthtoolCollector. +// This allows NewEthtoolTestCollector to override it's .stats interface +// for testing. +func makeEthtoolCollector(logger log.Logger) (*ethtoolCollector, error) { + fs, err := sysfs.NewFS(*sysPath) + if err != nil { + return nil, fmt.Errorf("failed to open sysfs: %w", err) + } + + // Pre-populate some common ethtool metrics. + return ðtoolCollector{ + fs: fs, + stats: ðtoolStats{}, + entries: map[string]*prometheus.Desc{ + "rx_bytes": prometheus.NewDesc( + "node_ethtool_received_bytes_total", + "Network interface bytes received", + []string{"device"}, nil, + ), + "rx_dropped": prometheus.NewDesc( + "node_ethtool_received_dropped_total", + "Number of received frames dropped", + []string{"device"}, nil, + ), + "rx_errors": prometheus.NewDesc( + "node_ethtool_received_errors_total", + "Number of received frames with errors", + []string{"device"}, nil, + ), + "rx_packets": prometheus.NewDesc( + "node_ethtool_received_packets_total", + "Network interface packets received", + []string{"device"}, nil, + ), + "tx_bytes": prometheus.NewDesc( + "node_ethtool_transmitted_bytes_total", + "Network interface bytes sent", + []string{"device"}, nil, + ), + "tx_errors": prometheus.NewDesc( + "node_ethtool_transmitted_errors_total", + "Number of sent frames with errors", + []string{"device"}, nil, + ), + "tx_packets": prometheus.NewDesc( + "node_ethtool_transmitted_packets_total", + "Network interface packets sent", + []string{"device"}, nil, + ), + }, + logger: logger, + }, nil +} + +func init() { + registerCollector("ethtool", defaultDisabled, NewEthtoolCollector) +} + +// NewEthtoolCollector returns a new Collector exposing ethtool stats. +func NewEthtoolCollector(logger log.Logger) (Collector, error) { + // Specifying --collector.ethtool.fixtures on the command line activates + // the test fixtures. This is for `end-to-end-test.sh` + if *ethtoolFixtures != "" { + return NewEthtoolTestCollector(logger) + } + return makeEthtoolCollector(logger) +} + +func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error { + netClass, err := c.fs.NetClass() + if err != nil { + if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) { + level.Debug(c.logger).Log("msg", "Could not read netclass file", "err", err) + return ErrNoData + } + return fmt.Errorf("could not get net class info: %w", err) + } + + if len(netClass) == 0 { + return fmt.Errorf("no network devices found") + } + + for device := range netClass { + var stats map[string]uint64 + var err error + + stats, err = c.stats.Stats(device) + if err != nil { + // Suppressing errors because it's hard to tell what interfaces support ethtool and which don't. + continue + } + + // Sort metric names so that the test fixtures will match up + keys := make([]string, 0, len(stats)) + for k := range stats { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, metric := range keys { + val := stats[metric] + metricFQName := prometheus.BuildFQName(namespace, "ethtool", metric) + metricFQName = receivedRegex.ReplaceAllString(metricFQName, "_received_") + metricFQName = transmittedRegex.ReplaceAllString(metricFQName, "_transmitted_") + + // Check to see if this metric exists; if not then create it and store it in c.entries. + entry, exists := c.entries[metric] + if !exists { + entry = prometheus.NewDesc( + metricFQName, + fmt.Sprintf("Network interface %s", metric), + []string{"device"}, nil, + ) + c.entries[metric] = entry + } + ch <- prometheus.MustNewConstMetric( + entry, prometheus.UntypedValue, float64(val), device) + } + } + + return nil +} diff --git a/collector/ethtool_linux_test.go b/collector/ethtool_linux_test.go new file mode 100644 index 00000000..28f1b70e --- /dev/null +++ b/collector/ethtool_linux_test.go @@ -0,0 +1,70 @@ +// Copyright 2021 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "fmt" + "testing" + + "github.com/go-kit/kit/log" + "github.com/prometheus/client_golang/prometheus" +) + +func TestEthtoolCollector(t *testing.T) { + testcases := []string{ + prometheus.NewDesc("node_ethtool_align_errors", "Network interface align_errors", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_broadcast", "Network interface rx_broadcast", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_errors_total", "Number of received frames with errors", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_missed", "Network interface rx_missed", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_multicast", "Network interface rx_multicast", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_packets_total", "Network interface packets received", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_received_unicast", "Network interface rx_unicast", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_transmitted_aborted", "Network interface tx_aborted", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_transmitted_errors_total", "Number of sent frames with errors", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_transmitted_multi_collisions", "Network interface tx_multi_collisions", []string{"device"}, nil).String(), + prometheus.NewDesc("node_ethtool_transmitted_packets_total", "Network interface packets sent", []string{"device"}, nil).String(), + } + + *sysPath = "fixtures/sys" + *ethtoolFixtures = "fixtures/ethtool/" + + collector, err := NewEthtoolTestCollector(log.NewNopLogger()) + if err != nil { + panic(err) + } + + sink := make(chan prometheus.Metric) + go func() { + err = collector.Update(sink) + if err != nil { + panic(fmt.Errorf("failed to update collector: %s", err)) + } + close(sink) + }() + + for _, expected := range testcases { + metric := (<-sink) + if metric == nil { + t.Fatalf("Expected '%s' but got nothing (nil).", expected) + } + + got := metric.Desc().String() + metric.Desc() + if expected != got { + t.Errorf("Expected '%s' but got '%s'", expected, got) + } else { + t.Logf("Successfully got '%s'", got) + } + } +} diff --git a/collector/fixtures/e2e-output.txt b/collector/fixtures/e2e-output.txt index e04cb0cc..5f3c3e0e 100644 --- a/collector/fixtures/e2e-output.txt +++ b/collector/fixtures/e2e-output.txt @@ -657,6 +657,45 @@ node_entropy_available_bits 1337 # HELP node_entropy_pool_size_bits Bits of entropy pool. # TYPE node_entropy_pool_size_bits gauge node_entropy_pool_size_bits 4096 +# 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_received_broadcast Network interface rx_broadcast +# TYPE node_ethtool_received_broadcast untyped +node_ethtool_received_broadcast{device="eth0"} 5792 +# HELP node_ethtool_received_errors_total Number of received frames with errors +# TYPE node_ethtool_received_errors_total untyped +node_ethtool_received_errors_total{device="eth0"} 0 +# HELP node_ethtool_received_missed Network interface rx_missed +# TYPE node_ethtool_received_missed untyped +node_ethtool_received_missed{device="eth0"} 401 +# HELP node_ethtool_received_multicast Network interface rx_multicast +# TYPE node_ethtool_received_multicast untyped +node_ethtool_received_multicast{device="eth0"} 23973 +# HELP node_ethtool_received_packets_total Network interface packets received +# TYPE node_ethtool_received_packets_total untyped +node_ethtool_received_packets_total{device="eth0"} 1.260062e+06 +# HELP node_ethtool_received_unicast Network interface rx_unicast +# TYPE node_ethtool_received_unicast untyped +node_ethtool_received_unicast{device="eth0"} 1.230297e+06 +# HELP node_ethtool_transmitted_aborted Network interface tx_aborted +# TYPE node_ethtool_transmitted_aborted untyped +node_ethtool_transmitted_aborted{device="eth0"} 0 +# HELP node_ethtool_transmitted_errors_total Number of sent frames with errors +# TYPE node_ethtool_transmitted_errors_total untyped +node_ethtool_transmitted_errors_total{device="eth0"} 0 +# HELP node_ethtool_transmitted_multi_collisions Network interface tx_multi_collisions +# TYPE node_ethtool_transmitted_multi_collisions untyped +node_ethtool_transmitted_multi_collisions{device="eth0"} 0 +# HELP node_ethtool_transmitted_packets_total Network interface packets sent +# TYPE node_ethtool_transmitted_packets_total untyped +node_ethtool_transmitted_packets_total{device="eth0"} 961500 +# HELP node_ethtool_transmitted_single_collisions Network interface tx_single_collisions +# TYPE node_ethtool_transmitted_single_collisions untyped +node_ethtool_transmitted_single_collisions{device="eth0"} 0 +# HELP node_ethtool_transmitted_underrun Network interface tx_underrun +# TYPE node_ethtool_transmitted_underrun untyped +node_ethtool_transmitted_underrun{device="eth0"} 0 # HELP node_exporter_build_info A metric with a constant '1' value labeled by version, revision, branch, and goversion from which node_exporter was built. # TYPE node_exporter_build_info gauge # HELP node_fibrechannel_error_frames_total Number of errors in frames @@ -2745,6 +2784,7 @@ node_scrape_collector_success{collector="diskstats"} 1 node_scrape_collector_success{collector="drbd"} 1 node_scrape_collector_success{collector="edac"} 1 node_scrape_collector_success{collector="entropy"} 1 +node_scrape_collector_success{collector="ethtool"} 1 node_scrape_collector_success{collector="fibrechannel"} 1 node_scrape_collector_success{collector="filefd"} 1 node_scrape_collector_success{collector="hwmon"} 1 diff --git a/collector/fixtures/ethtool/eth0 b/collector/fixtures/ethtool/eth0 new file mode 100644 index 00000000..9ce7e015 --- /dev/null +++ b/collector/fixtures/ethtool/eth0 @@ -0,0 +1,15 @@ +# ethtool -S eth0 +NIC statistics: + tx_packets: 961500 + rx_packets: 1260062 + tx_errors: 0 + rx_errors: 0 + rx_missed: 401 + align_errors: 0 + tx_single_collisions: 0 + tx_multi_collisions: 0 + rx_unicast: 1230297 + rx_broadcast: 5792 + rx_multicast: 23973 + tx_aborted: 0 + tx_underrun: 0 diff --git a/end-to-end-test.sh b/end-to-end-test.sh index d87162fe..9de1e980 100755 --- a/end-to-end-test.sh +++ b/end-to-end-test.sh @@ -14,6 +14,7 @@ enabled_collectors=$(cat << COLLECTORS drbd edac entropy + ethtool fibrechannel filefd hwmon @@ -109,6 +110,7 @@ fi --collector.qdisc.fixtures="collector/fixtures/qdisc/" \ --collector.netclass.ignored-devices="(dmz|int)" \ --collector.netclass.ignore-invalid-speed \ + --collector.ethtool.fixtures="collector/fixtures/ethtool/" \ --collector.bcache.priorityStats \ --collector.cpu.info \ --collector.cpu.info.flags-include="^(aes|avx.?|constant_tsc)$" \ diff --git a/go.mod b/go.mod index 13c36ebc..a79dcb0e 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/prometheus/common v0.15.0 github.com/prometheus/exporter-toolkit v0.5.1 github.com/prometheus/procfs v0.6.0 + github.com/safchain/ethtool v0.0.0-20200804214954-8f958a28363a github.com/siebenmann/go-kstat v0.0.0-20200303194639-4e8294f9e9d5 github.com/soundcloud/go-runit v0.0.0-20150630195641-06ad41a06c4a golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c diff --git a/go.sum b/go.sum index 37f928be..9a4efe45 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20200804214954-8f958a28363a h1:TXFp1qmI50hk8dfGl7o8PKT9Lxo84T7RlpKCOB1DndI= +github.com/safchain/ethtool v0.0.0-20200804214954-8f958a28363a/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= From 807f3c3af3263762030a39f62e4a9912f678ca7a Mon Sep 17 00:00:00 2001 From: "W. Andrew Denton" Date: Mon, 3 May 2021 09:35:49 -0700 Subject: [PATCH 2/5] ethtool: Remove end-to-end testing. Signed-off-by: W. Andrew Denton --- collector/ethtool_linux.go | 62 ------------------------------- collector/ethtool_linux_test.go | 57 +++++++++++++++++++++++++++- collector/fixtures/e2e-output.txt | 40 -------------------- end-to-end-test.sh | 2 - 4 files changed, 56 insertions(+), 105 deletions(-) diff --git a/collector/ethtool_linux.go b/collector/ethtool_linux.go index 912e31e5..3e01c29c 100644 --- a/collector/ethtool_linux.go +++ b/collector/ethtool_linux.go @@ -20,29 +20,22 @@ package collector import ( - "bufio" "errors" "fmt" "os" - "path/filepath" "regexp" "sort" - "strconv" - "strings" - "syscall" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/procfs/sysfs" "github.com/safchain/ethtool" - "gopkg.in/alecthomas/kingpin.v2" ) var ( receivedRegex = regexp.MustCompile(`_rx_`) transmittedRegex = regexp.MustCompile(`_tx_`) - ethtoolFixtures = kingpin.Flag("collector.ethtool.fixtures", "test fixtures to use for ethtool collector end-to-end testing").Default("").String() ) type EthtoolStats interface { @@ -63,56 +56,6 @@ type ethtoolCollector struct { stats EthtoolStats } -type EthtoolFixture struct { - fixturePath string -} - -func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { - res := make(map[string]uint64) - - fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf)) - if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { - // The fixture for this interface doesn't exist. That's OK because it replicates - // an interface that doesn't support ethtool. - return res, nil - } - if err != nil { - return res, err - } - defer fixtureFile.Close() - - scanner := bufio.NewScanner(fixtureFile) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "#") { - continue - } - if strings.HasPrefix(line, "NIC statistics:") { - continue - } - line = strings.Trim(line, " ") - items := strings.Split(line, ": ") - val, err := strconv.ParseUint(items[1], 10, 64) - if err != nil { - return res, err - } - res[items[0]] = val - } - - return res, err -} - -func NewEthtoolTestCollector(logger log.Logger) (Collector, error) { - collector, err := makeEthtoolCollector(logger) - collector.stats = &EthtoolFixture{ - fixturePath: *ethtoolFixtures, - } - if err != nil { - return nil, err - } - return collector, nil -} - // makeEthtoolCollector is the internal constructor for EthtoolCollector. // This allows NewEthtoolTestCollector to override it's .stats interface // for testing. @@ -173,11 +116,6 @@ func init() { // NewEthtoolCollector returns a new Collector exposing ethtool stats. func NewEthtoolCollector(logger log.Logger) (Collector, error) { - // Specifying --collector.ethtool.fixtures on the command line activates - // the test fixtures. This is for `end-to-end-test.sh` - if *ethtoolFixtures != "" { - return NewEthtoolTestCollector(logger) - } return makeEthtoolCollector(logger) } diff --git a/collector/ethtool_linux_test.go b/collector/ethtool_linux_test.go index 28f1b70e..8aaeed3a 100644 --- a/collector/ethtool_linux_test.go +++ b/collector/ethtool_linux_test.go @@ -14,13 +14,69 @@ package collector import ( + "bufio" "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" "testing" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" ) +type EthtoolFixture struct { + fixturePath string +} + +func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { + res := make(map[string]uint64) + + fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf)) + if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { + // The fixture for this interface doesn't exist. That's OK because it replicates + // an interface that doesn't support ethtool. + return res, nil + } + if err != nil { + return res, err + } + defer fixtureFile.Close() + + scanner := bufio.NewScanner(fixtureFile) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "NIC statistics:") { + continue + } + line = strings.Trim(line, " ") + items := strings.Split(line, ": ") + val, err := strconv.ParseUint(items[1], 10, 64) + if err != nil { + return res, err + } + res[items[0]] = val + } + + return res, err +} + +func NewEthtoolTestCollector(logger log.Logger) (Collector, error) { + collector, err := makeEthtoolCollector(logger) + collector.stats = &EthtoolFixture{ + fixturePath: "fixtures/ethtool/", + } + if err != nil { + return nil, err + } + return collector, nil +} + func TestEthtoolCollector(t *testing.T) { testcases := []string{ prometheus.NewDesc("node_ethtool_align_errors", "Network interface align_errors", []string{"device"}, nil).String(), @@ -37,7 +93,6 @@ func TestEthtoolCollector(t *testing.T) { } *sysPath = "fixtures/sys" - *ethtoolFixtures = "fixtures/ethtool/" collector, err := NewEthtoolTestCollector(log.NewNopLogger()) if err != nil { diff --git a/collector/fixtures/e2e-output.txt b/collector/fixtures/e2e-output.txt index 5f3c3e0e..e04cb0cc 100644 --- a/collector/fixtures/e2e-output.txt +++ b/collector/fixtures/e2e-output.txt @@ -657,45 +657,6 @@ node_entropy_available_bits 1337 # HELP node_entropy_pool_size_bits Bits of entropy pool. # TYPE node_entropy_pool_size_bits gauge node_entropy_pool_size_bits 4096 -# 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_received_broadcast Network interface rx_broadcast -# TYPE node_ethtool_received_broadcast untyped -node_ethtool_received_broadcast{device="eth0"} 5792 -# HELP node_ethtool_received_errors_total Number of received frames with errors -# TYPE node_ethtool_received_errors_total untyped -node_ethtool_received_errors_total{device="eth0"} 0 -# HELP node_ethtool_received_missed Network interface rx_missed -# TYPE node_ethtool_received_missed untyped -node_ethtool_received_missed{device="eth0"} 401 -# HELP node_ethtool_received_multicast Network interface rx_multicast -# TYPE node_ethtool_received_multicast untyped -node_ethtool_received_multicast{device="eth0"} 23973 -# HELP node_ethtool_received_packets_total Network interface packets received -# TYPE node_ethtool_received_packets_total untyped -node_ethtool_received_packets_total{device="eth0"} 1.260062e+06 -# HELP node_ethtool_received_unicast Network interface rx_unicast -# TYPE node_ethtool_received_unicast untyped -node_ethtool_received_unicast{device="eth0"} 1.230297e+06 -# HELP node_ethtool_transmitted_aborted Network interface tx_aborted -# TYPE node_ethtool_transmitted_aborted untyped -node_ethtool_transmitted_aborted{device="eth0"} 0 -# HELP node_ethtool_transmitted_errors_total Number of sent frames with errors -# TYPE node_ethtool_transmitted_errors_total untyped -node_ethtool_transmitted_errors_total{device="eth0"} 0 -# HELP node_ethtool_transmitted_multi_collisions Network interface tx_multi_collisions -# TYPE node_ethtool_transmitted_multi_collisions untyped -node_ethtool_transmitted_multi_collisions{device="eth0"} 0 -# HELP node_ethtool_transmitted_packets_total Network interface packets sent -# TYPE node_ethtool_transmitted_packets_total untyped -node_ethtool_transmitted_packets_total{device="eth0"} 961500 -# HELP node_ethtool_transmitted_single_collisions Network interface tx_single_collisions -# TYPE node_ethtool_transmitted_single_collisions untyped -node_ethtool_transmitted_single_collisions{device="eth0"} 0 -# HELP node_ethtool_transmitted_underrun Network interface tx_underrun -# TYPE node_ethtool_transmitted_underrun untyped -node_ethtool_transmitted_underrun{device="eth0"} 0 # HELP node_exporter_build_info A metric with a constant '1' value labeled by version, revision, branch, and goversion from which node_exporter was built. # TYPE node_exporter_build_info gauge # HELP node_fibrechannel_error_frames_total Number of errors in frames @@ -2784,7 +2745,6 @@ node_scrape_collector_success{collector="diskstats"} 1 node_scrape_collector_success{collector="drbd"} 1 node_scrape_collector_success{collector="edac"} 1 node_scrape_collector_success{collector="entropy"} 1 -node_scrape_collector_success{collector="ethtool"} 1 node_scrape_collector_success{collector="fibrechannel"} 1 node_scrape_collector_success{collector="filefd"} 1 node_scrape_collector_success{collector="hwmon"} 1 diff --git a/end-to-end-test.sh b/end-to-end-test.sh index 9de1e980..d87162fe 100755 --- a/end-to-end-test.sh +++ b/end-to-end-test.sh @@ -14,7 +14,6 @@ enabled_collectors=$(cat << COLLECTORS drbd edac entropy - ethtool fibrechannel filefd hwmon @@ -110,7 +109,6 @@ fi --collector.qdisc.fixtures="collector/fixtures/qdisc/" \ --collector.netclass.ignored-devices="(dmz|int)" \ --collector.netclass.ignore-invalid-speed \ - --collector.ethtool.fixtures="collector/fixtures/ethtool/" \ --collector.bcache.priorityStats \ --collector.cpu.info \ --collector.cpu.info.flags-include="^(aes|avx.?|constant_tsc)$" \ From 892893ff05c5abc964c4d910c1c66e117ed8afb6 Mon Sep 17 00:00:00 2001 From: "W. Andrew Denton" Date: Fri, 14 May 2021 10:07:30 -0700 Subject: [PATCH 3/5] ethtool: Log stats collection errors. Signed-off-by: W. Andrew Denton --- collector/ethtool_linux.go | 19 ++++++++++++++++++- collector/ethtool_linux_test.go | 10 +++++++--- collector/fixtures/ethtool/bond0 | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 collector/fixtures/ethtool/bond0 diff --git a/collector/ethtool_linux.go b/collector/ethtool_linux.go index 3e01c29c..f5613491 100644 --- a/collector/ethtool_linux.go +++ b/collector/ethtool_linux.go @@ -25,12 +25,14 @@ import ( "os" "regexp" "sort" + "syscall" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/procfs/sysfs" "github.com/safchain/ethtool" + "golang.org/x/sys/unix" ) var ( @@ -138,8 +140,23 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error { var err error stats, err = c.stats.Stats(device) + + // If Stats() returns EOPNOTSUPP it doesn't support ethtool stats. Log that only at Debug level. + // Otherwise log it at Error level. if err != nil { - // Suppressing errors because it's hard to tell what interfaces support ethtool and which don't. + if errno, ok := err.(syscall.Errno); ok { + if err == unix.EOPNOTSUPP { + level.Debug(c.logger).Log("msg", "ethtool stats error", "err", err, "device", device, "errno", uint(errno)) + } else if errno != 0 { + level.Error(c.logger).Log("msg", "ethtool stats error", "err", err, "device", device, "errno", uint(errno)) + } + } else { + level.Error(c.logger).Log("msg", "ethtool stats error", "err", err, "device", device) + } + } + + if stats == nil || len(stats) < 1 { + // No stats returned; device does not support ethtool stats. continue } diff --git a/collector/ethtool_linux_test.go b/collector/ethtool_linux_test.go index 8aaeed3a..d8a5593b 100644 --- a/collector/ethtool_linux_test.go +++ b/collector/ethtool_linux_test.go @@ -25,6 +25,7 @@ import ( "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sys/unix" ) type EthtoolFixture struct { @@ -36,9 +37,9 @@ func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf)) if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { - // The fixture for this interface doesn't exist. That's OK because it replicates - // an interface that doesn't support ethtool. - return res, nil + // The fixture for this interface doesn't exist. Translate that to unix.EOPNOTSUPP + // to replicate an interface that doesn't support ethtool stats + return res, unix.EOPNOTSUPP } if err != nil { return res, err @@ -60,6 +61,9 @@ func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { if err != nil { return res, err } + if items[0] == "ERROR" { + return res, unix.Errno(val) + } res[items[0]] = val } diff --git a/collector/fixtures/ethtool/bond0 b/collector/fixtures/ethtool/bond0 new file mode 100644 index 00000000..42e4a141 --- /dev/null +++ b/collector/fixtures/ethtool/bond0 @@ -0,0 +1 @@ +ERROR: 1 \ No newline at end of file From 5cf7968ad142271976b1a8750319e30597eb3b6d Mon Sep 17 00:00:00 2001 From: "W. Andrew Denton" Date: Tue, 18 May 2021 08:50:45 -0700 Subject: [PATCH 4/5] ethtool: Add documentation to README.md Signed-off-by: W. Andrew Denton --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a1539bc4..f1966513 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Name | Description | OS buddyinfo | Exposes statistics of memory fragments as reported by /proc/buddyinfo. | Linux devstat | Exposes device statistics | Dragonfly, FreeBSD drbd | Exposes Distributed Replicated Block Device statistics (to version 8.4) | Linux +ethtool | Exposes network interface and network driver statistics equivalent to `ethtool -S`. | Linux interrupts | Exposes detailed interrupts statistics. | Linux, OpenBSD ksmd | Exposes kernel and system statistics from `/sys/kernel/mm/ksm`. | Linux logind | Exposes session counts from [logind](http://www.freedesktop.org/wiki/Software/systemd/logind/). | Linux From 76c0e1e5a128c5bbdff8eeb9e49313f008170397 Mon Sep 17 00:00:00 2001 From: ventifus <44683696+ventifus@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:02:08 -0700 Subject: [PATCH 5/5] Update collector/ethtool_linux.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: W. Andrew Denton Co-authored-by: Manuel RĂ¼ger --- collector/ethtool_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collector/ethtool_linux.go b/collector/ethtool_linux.go index f5613491..dfe69627 100644 --- a/collector/ethtool_linux.go +++ b/collector/ethtool_linux.go @@ -59,7 +59,7 @@ type ethtoolCollector struct { } // makeEthtoolCollector is the internal constructor for EthtoolCollector. -// This allows NewEthtoolTestCollector to override it's .stats interface +// This allows NewEthtoolTestCollector to override its .stats interface // for testing. func makeEthtoolCollector(logger log.Logger) (*ethtoolCollector, error) { fs, err := sysfs.NewFS(*sysPath)