// Copyright 2015 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. //go:build !nonetdev && (linux || freebsd || openbsd || dragonfly || darwin || aix) // +build !nonetdev // +build linux freebsd openbsd dragonfly darwin aix package collector import ( "errors" "fmt" "log/slog" "net" "strconv" "sync" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" ) var ( netdevDeviceInclude = kingpin.Flag("collector.netdev.device-include", "Regexp of net devices to include (mutually exclusive to device-exclude).").String() oldNetdevDeviceInclude = kingpin.Flag("collector.netdev.device-whitelist", "DEPRECATED: Use collector.netdev.device-include").Hidden().String() netdevDeviceExclude = kingpin.Flag("collector.netdev.device-exclude", "Regexp of net devices to exclude (mutually exclusive to device-include).").String() oldNetdevDeviceExclude = kingpin.Flag("collector.netdev.device-blacklist", "DEPRECATED: Use collector.netdev.device-exclude").Hidden().String() netdevAddressInfo = kingpin.Flag("collector.netdev.address-info", "Collect address-info for every device").Bool() netdevDetailedMetrics = kingpin.Flag("collector.netdev.enable-detailed-metrics", "Use (incompatible) metric names that provide more detailed stats on Linux").Bool() ) type netDevCollector struct { subsystem string deviceFilter deviceFilter metricDescsMutex sync.Mutex metricDescs map[string]*prometheus.Desc logger *slog.Logger } type netDevStats map[string]map[string]uint64 func init() { registerCollector("netdev", defaultEnabled, NewNetDevCollector) } // NewNetDevCollector returns a new Collector exposing network device stats. func NewNetDevCollector(logger *slog.Logger) (Collector, error) { if *oldNetdevDeviceInclude != "" { if *netdevDeviceInclude == "" { logger.Warn("--collector.netdev.device-whitelist is DEPRECATED and will be removed in 2.0.0, use --collector.netdev.device-include") *netdevDeviceInclude = *oldNetdevDeviceInclude } else { return nil, errors.New("--collector.netdev.device-whitelist and --collector.netdev.device-include are mutually exclusive") } } if *oldNetdevDeviceExclude != "" { if *netdevDeviceExclude == "" { logger.Warn("--collector.netdev.device-blacklist is DEPRECATED and will be removed in 2.0.0, use --collector.netdev.device-exclude") *netdevDeviceExclude = *oldNetdevDeviceExclude } else { return nil, errors.New("--collector.netdev.device-blacklist and --collector.netdev.device-exclude are mutually exclusive") } } if *netdevDeviceExclude != "" && *netdevDeviceInclude != "" { return nil, errors.New("device-exclude & device-include are mutually exclusive") } if *netdevDeviceExclude != "" { logger.Info("Parsed flag --collector.netdev.device-exclude", "flag", *netdevDeviceExclude) } if *netdevDeviceInclude != "" { logger.Info("Parsed Flag --collector.netdev.device-include", "flag", *netdevDeviceInclude) } return &netDevCollector{ subsystem: "network", deviceFilter: newDeviceFilter(*netdevDeviceExclude, *netdevDeviceInclude), metricDescs: map[string]*prometheus.Desc{}, logger: logger, }, nil } func (c *netDevCollector) metricDesc(key string, labels []string) *prometheus.Desc { c.metricDescsMutex.Lock() defer c.metricDescsMutex.Unlock() if _, ok := c.metricDescs[key]; !ok { c.metricDescs[key] = prometheus.NewDesc( prometheus.BuildFQName(namespace, c.subsystem, key+"_total"), fmt.Sprintf("Network device statistic %s.", key), labels, nil, ) } return c.metricDescs[key] } func (c *netDevCollector) Update(ch chan<- prometheus.Metric) error { netDev, err := getNetDevStats(&c.deviceFilter, c.logger) if err != nil { return fmt.Errorf("couldn't get netstats: %w", err) } netDevLabels, err := getNetDevLabels() if err != nil { return fmt.Errorf("couldn't get netdev labels: %w", err) } for dev, devStats := range netDev { if !*netdevDetailedMetrics { legacy(devStats) } labels := []string{"device"} labelValues := []string{dev} if devLabels, exists := netDevLabels[dev]; exists { for labelName, labelValue := range devLabels { labels = append(labels, labelName) labelValues = append(labelValues, labelValue) } } for key, value := range devStats { desc := c.metricDesc(key, labels) ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, float64(value), labelValues...) } } if *netdevAddressInfo { interfaces, err := net.Interfaces() if err != nil { return fmt.Errorf("could not get network interfaces: %w", err) } desc := prometheus.NewDesc(prometheus.BuildFQName(namespace, "network_address", "info"), "node network address by device", []string{"device", "address", "netmask", "scope"}, nil) for _, addr := range getAddrsInfo(interfaces) { ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, 1, addr.device, addr.addr, addr.netmask, addr.scope) } } return nil } type addrInfo struct { device string addr string scope string netmask string } func scope(ip net.IP) string { if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return "link-local" } if ip.IsInterfaceLocalMulticast() { return "interface-local" } if ip.IsGlobalUnicast() { return "global" } return "" } // getAddrsInfo returns interface name, address, scope and netmask for all interfaces. func getAddrsInfo(interfaces []net.Interface) []addrInfo { var res []addrInfo for _, ifs := range interfaces { addrs, _ := ifs.Addrs() for _, addr := range addrs { ip, ipNet, err := net.ParseCIDR(addr.String()) if err != nil { continue } size, _ := ipNet.Mask.Size() res = append(res, addrInfo{ device: ifs.Name, addr: ip.String(), scope: scope(ip), netmask: strconv.Itoa(size), }) } } return res } // https://github.com/torvalds/linux/blob/master/net/core/net-procfs.c#L75-L97 func legacy(metrics map[string]uint64) { if metric, ok := pop(metrics, "receive_errors"); ok { metrics["receive_errs"] = metric } if metric, ok := pop(metrics, "receive_dropped"); ok { metrics["receive_drop"] = metric + popz(metrics, "receive_missed_errors") } if metric, ok := pop(metrics, "receive_fifo_errors"); ok { metrics["receive_fifo"] = metric } if metric, ok := pop(metrics, "receive_frame_errors"); ok { metrics["receive_frame"] = metric + popz(metrics, "receive_length_errors") + popz(metrics, "receive_over_errors") + popz(metrics, "receive_crc_errors") } if metric, ok := pop(metrics, "multicast"); ok { metrics["receive_multicast"] = metric } if metric, ok := pop(metrics, "transmit_errors"); ok { metrics["transmit_errs"] = metric } if metric, ok := pop(metrics, "transmit_dropped"); ok { metrics["transmit_drop"] = metric } if metric, ok := pop(metrics, "transmit_fifo_errors"); ok { metrics["transmit_fifo"] = metric } if metric, ok := pop(metrics, "multicast"); ok { metrics["receive_multicast"] = metric } if metric, ok := pop(metrics, "collisions"); ok { metrics["transmit_colls"] = metric } if metric, ok := pop(metrics, "transmit_carrier_errors"); ok { metrics["transmit_carrier"] = metric + popz(metrics, "transmit_aborted_errors") + popz(metrics, "transmit_heartbeat_errors") + popz(metrics, "transmit_window_errors") } } func pop(m map[string]uint64, key string) (uint64, bool) { value, ok := m[key] delete(m, key) return value, ok } func popz(m map[string]uint64, key string) uint64 { if value, ok := m[key]; ok { delete(m, key) return value } return 0 }