Skip to content

Commit

Permalink
ethtool: Sanitize metric names
Browse files Browse the repository at this point in the history
OpenMetrics and the Prometheus exposition format require the metric name
to consist only of alphanumericals and "_", ":" and they must not start
with digits. The metric names from the ethtool stats might contain
spaces, brackets, and dots. Converting them directly to metric names
will produce invalid metric names.

Therefore sanitize the metric names and convert them to lower case.

Fixes: prometheus#2083
Signed-off-by: Benjamin Drung <benjamin.drung@ionos.com>
  • Loading branch information
bdrung authored and oblitorum committed Apr 9, 2024
1 parent c68cae9 commit 1b385c3
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 5 deletions.
33 changes: 28 additions & 5 deletions collector/ethtool_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"os"
"regexp"
"sort"
"strings"
"syscall"

"github.com/go-kit/log"
Expand All @@ -39,8 +40,9 @@ import (
var (
ethtoolIgnoredDevices = kingpin.Flag("collector.ethtool.ignored-devices", "Regexp of net devices to ignore for ethtool collector.").Default("^$").String()
ethtoolIncludedMetrics = kingpin.Flag("collector.ethtool.metrics-include", "Regexp of ethtool stats to include.").Default(".*").String()
receivedRegex = regexp.MustCompile(`_rx_`)
transmittedRegex = regexp.MustCompile(`_tx_`)
metricNameRegex = regexp.MustCompile(`_*[^0-9A-Za-z_]+_*`)
receivedRegex = regexp.MustCompile(`(^|_)rx(_|$)`)
transmittedRegex = regexp.MustCompile(`(^|_)tx(_|$)`)
)

type EthtoolStats interface {
Expand Down Expand Up @@ -123,6 +125,29 @@ func init() {
registerCollector("ethtool", defaultDisabled, NewEthtoolCollector)
}

// Sanitize the given metric name by replacing invalid characters by underscores.
//
// OpenMetrics and the Prometheus exposition format require the metric name
// to consist only of alphanumericals and "_", ":" and they must not start
// with digits. Since colons in MetricFamily are reserved to signal that the
// MetricFamily is the result of a calculation or aggregation of a general
// purpose monitoring system, colons will be replaced as well.
//
// Note: If not subsequently prepending a namespace and/or subsystem (e.g.,
// with prometheus.BuildFQName), the caller must ensure that the supplied
// metricName does not begin with a digit.
func SanitizeMetricName(metricName string) string {
return metricNameRegex.ReplaceAllString(metricName, "_")
}

// Generate the fully-qualified metric name for the ethool metric.
func buildEthtoolFQName(metric string) string {
metricName := strings.TrimLeft(strings.ToLower(SanitizeMetricName(metric)), "_")
metricName = receivedRegex.ReplaceAllString(metricName, "${1}received${2}")
metricName = transmittedRegex.ReplaceAllString(metricName, "${1}transmitted${2}")
return prometheus.BuildFQName(namespace, "ethtool", metricName)
}

// NewEthtoolCollector returns a new Collector exposing ethtool stats.
func NewEthtoolCollector(logger log.Logger) (Collector, error) {
return makeEthtoolCollector(logger)
Expand Down Expand Up @@ -183,9 +208,7 @@ func (c *ethtoolCollector) Update(ch chan<- prometheus.Metric) error {
continue
}
val := stats[metric]
metricFQName := prometheus.BuildFQName(namespace, "ethtool", metric)
metricFQName = receivedRegex.ReplaceAllString(metricFQName, "_received_")
metricFQName = transmittedRegex.ReplaceAllString(metricFQName, "_transmitted_")
metricFQName := buildEthtoolFQName(metric)

// Check to see if this metric exists; if not then create it and store it in c.entries.
entry, exists := c.entries[metric]
Expand Down
35 changes: 35 additions & 0 deletions collector/ethtool_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ func NewEthtoolTestCollector(logger log.Logger) (Collector, error) {
return collector, nil
}

func TestSanitizeMetricName(t *testing.T) {
testcases := map[string]string{
"": "",
"rx_errors": "rx_errors",
"Queue[0] AllocFails": "Queue_0_AllocFails",
"Tx LPI entry count": "Tx_LPI_entry_count",
"port.VF_admin_queue_requests": "port_VF_admin_queue_requests",
"[3]: tx_bytes": "_3_tx_bytes",
}

for metricName, expected := range testcases {
got := SanitizeMetricName(metricName)
if expected != got {
t.Errorf("Expected '%s' but got '%s'", expected, got)
}
}
}

func TestBuildEthtoolFQName(t *testing.T) {
testcases := map[string]string{
"rx_errors": "node_ethtool_received_errors",
"Queue[0] AllocFails": "node_ethtool_queue_0_allocfails",
"Tx LPI entry count": "node_ethtool_transmitted_lpi_entry_count",
"port.VF_admin_queue_requests": "node_ethtool_port_vf_admin_queue_requests",
"[3]: tx_bytes": "node_ethtool_3_transmitted_bytes",
}

for metric, expected := range testcases {
got := buildEthtoolFQName(metric)
if expected != got {
t.Errorf("Expected '%s' but got '%s'", expected, got)
}
}
}

func TestEthtoolCollector(t *testing.T) {
testcases := []string{
prometheus.NewDesc("node_ethtool_align_errors", "Network interface align_errors", []string{"device"}, nil).String(),
Expand Down

0 comments on commit 1b385c3

Please sign in to comment.