node_exporter/collector/ethtool_linux.go
Benjamin Drung 6ac6ea2d13 ethtool: Sanitize metric names
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: https://github.com/prometheus/node_exporter/issues/2083
Signed-off-by: Benjamin Drung <benjamin.drung@ionos.com>
2021-08-16 15:28:27 +02:00

230 lines
7.5 KiB
Go

// 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 (
"errors"
"fmt"
"os"
"regexp"
"sort"
"strings"
"syscall"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/procfs/sysfs"
"github.com/safchain/ethtool"
"golang.org/x/sys/unix"
"gopkg.in/alecthomas/kingpin.v2"
)
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()
metricNameRegex = regexp.MustCompile(`_*[^0-9A-Za-z_]+_*`)
receivedRegex = regexp.MustCompile(`(^|_)rx(_|$)`)
transmittedRegex = regexp.MustCompile(`(^|_)tx(_|$)`)
)
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
ignoredDevicesPattern *regexp.Regexp
metricsPattern *regexp.Regexp
logger log.Logger
stats EthtoolStats
}
// makeEthtoolCollector is the internal constructor for EthtoolCollector.
// This allows NewEthtoolTestCollector to override its .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 &ethtoolCollector{
fs: fs,
ignoredDevicesPattern: regexp.MustCompile(*ethtoolIgnoredDevices),
metricsPattern: regexp.MustCompile(*ethtoolIncludedMetrics),
logger: logger,
stats: &ethtoolStats{},
entries: map[string]*prometheus.Desc{
"rx_bytes": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "received_bytes_total"),
"Network interface bytes received",
[]string{"device"}, nil,
),
"rx_dropped": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "received_dropped_total"),
"Number of received frames dropped",
[]string{"device"}, nil,
),
"rx_errors": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "received_errors_total"),
"Number of received frames with errors",
[]string{"device"}, nil,
),
"rx_packets": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "received_packets_total"),
"Network interface packets received",
[]string{"device"}, nil,
),
"tx_bytes": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "transmitted_bytes_total"),
"Network interface bytes sent",
[]string{"device"}, nil,
),
"tx_errors": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "transmitted_errors_total"),
"Number of sent frames with errors",
[]string{"device"}, nil,
),
"tx_packets": prometheus.NewDesc(
prometheus.BuildFQName(namespace, "ethtool", "transmitted_packets_total"),
"Network interface packets sent",
[]string{"device"}, nil,
),
},
}, nil
}
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)
}
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
if c.ignoredDevicesPattern.MatchString(device) {
continue
}
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 {
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
}
// 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 {
if !c.metricsPattern.MatchString(metric) {
continue
}
val := stats[metric]
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]
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
}