// Copyright 2022 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 !nonetclass && linux
// +build !nonetclass,linux
package collector
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"github.com/alecthomas/kingpin/v2"
"github.com/jsimonetti/rtnetlink/v2"
"github.com/mdlayher/ethtool"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/procfs/sysfs"
)
var (
netclassRTNLWithStats = kingpin . Flag ( "collector.netclass_rtnl.with-stats" , "Expose the statistics for each network device, replacing netdev collector." ) . Bool ( )
operstateStr = [ ] string {
"unknown" , "notpresent" , "down" , "lowerlayerdown" , "testing" ,
"dormant" , "up" ,
}
)
func ( c * netClassCollector ) netClassRTNLUpdate ( ch chan <- prometheus . Metric ) error {
linkModes := make ( map [ string ] * ethtool . LinkMode )
lms , err := c . getLinkModes ( )
if err != nil {
if ! errors . Is ( errors . Unwrap ( err ) , fs . ErrNotExist ) {
return fmt . Errorf ( "could not get link modes: %w" , err )
}
c . logger . Info ( "ETHTOOL netlink interface unavailable, duplex and linkspeed are not scraped." )
} else {
for _ , lm := range lms {
if c . ignoredDevicesPattern . MatchString ( lm . Interface . Name ) {
continue
}
if lm . SpeedMegabits >= 0 {
speedBytes := uint64 ( lm . SpeedMegabits * 1000 * 1000 / 8 )
pushMetric ( ch , c . getFieldDesc ( "speed_bytes" ) , "speed_bytes" , speedBytes , prometheus . GaugeValue , lm . Interface . Name )
}
linkModes [ lm . Interface . Name ] = lm
}
}
// Get most attributes from Netlink
lMsgs , err := c . getNetClassInfoRTNL ( )
if err != nil {
return fmt . Errorf ( "could not get net class info: %w" , err )
}
relevantLinks := make ( [ ] rtnetlink . LinkMessage , 0 , len ( lMsgs ) )
for _ , msg := range lMsgs {
if ! c . ignoredDevicesPattern . MatchString ( msg . Attributes . Name ) {
relevantLinks = append ( relevantLinks , msg )
}
}
// Read sysfs for attributes that Netlink doesn't expose
sysfsAttrs , err := getSysfsAttributes ( relevantLinks )
if err != nil {
return fmt . Errorf ( "could not get sysfs device info: %w" , err )
}
// Parse all the info and update metrics
for _ , msg := range relevantLinks {
upDesc := prometheus . NewDesc (
prometheus . BuildFQName ( namespace , c . subsystem , "up" ) ,
"Value is 1 if operstate is 'up', 0 otherwise." ,
[ ] string { "device" } ,
nil ,
)
upValue := 0.0
if msg . Attributes . OperationalState == rtnetlink . OperStateUp {
upValue = 1.0
}
ch <- prometheus . MustNewConstMetric ( upDesc , prometheus . GaugeValue , upValue , msg . Attributes . Name )
infoDesc := prometheus . NewDesc (
prometheus . BuildFQName ( namespace , c . subsystem , "info" ) ,
"Non-numeric data of <iface>, value is always 1." ,
[ ] string { "device" , "address" , "broadcast" , "duplex" , "operstate" , "ifalias" } ,
nil ,
)
infoValue := 1.0
var ifalias = ""
if msg . Attributes . Alias != nil {
ifalias = * msg . Attributes . Alias
}
duplex := ""
lm , lmExists := linkModes [ msg . Attributes . Name ]
if lmExists {
duplex = lm . Duplex . String ( )
}
ifaceInfo := sysfsAttrs [ msg . Attributes . Name ]
ch <- prometheus . MustNewConstMetric ( infoDesc , prometheus . GaugeValue , infoValue , msg . Attributes . Name , msg . Attributes . Address . String ( ) , msg . Attributes . Broadcast . String ( ) , duplex , operstateStr [ int ( msg . Attributes . OperationalState ) ] , ifalias )
pushMetric ( ch , c . getFieldDesc ( "address_assign_type" ) , "address_assign_type" , ifaceInfo . AddrAssignType , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "carrier" ) , "carrier" , msg . Attributes . Carrier , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "carrier_changes_total" ) , "carrier_changes_total" , msg . Attributes . CarrierChanges , prometheus . CounterValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "carrier_up_changes_total" ) , "carrier_up_changes_total" , msg . Attributes . CarrierUpCount , prometheus . CounterValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "carrier_down_changes_total" ) , "carrier_down_changes_total" , msg . Attributes . CarrierDownCount , prometheus . CounterValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "device_id" ) , "device_id" , ifaceInfo . DevID , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "flags" ) , "flags" , msg . Flags , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "iface_id" ) , "iface_id" , msg . Index , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "iface_link_mode" ) , "iface_link_mode" , msg . Attributes . LinkMode , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "dormant" ) , "dormant" , msg . Attributes . LinkMode , prometheus . GaugeValue , msg . Attributes . Name )
// kernel logic: IFLA_LINK attribute will be ignore when ifindex is the same as iflink
// (dev->ifindex != dev_get_iflink(dev) && nla_put_u32(skb, IFLA_LINK, dev_get_iflink(dev)))
// As interface ID is never 0, we assume msg.Attributes.Type 0 means iflink is omitted in RTM_GETLINK response.
if msg . Attributes . Type > 0 {
pushMetric ( ch , c . getFieldDesc ( "iface_link" ) , "iface_link" , msg . Attributes . Type , prometheus . GaugeValue , msg . Attributes . Name )
} else {
pushMetric ( ch , c . getFieldDesc ( "iface_link" ) , "iface_link" , msg . Index , prometheus . GaugeValue , msg . Attributes . Name )
}
pushMetric ( ch , c . getFieldDesc ( "mtu_bytes" ) , "mtu_bytes" , msg . Attributes . MTU , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "name_assign_type" ) , "name_assign_type" , ifaceInfo . NameAssignType , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "net_dev_group" ) , "net_dev_group" , msg . Attributes . NetDevGroup , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_queue_length" ) , "transmit_queue_length" , msg . Attributes . TxQueueLen , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "protocol_type" ) , "protocol_type" , msg . Type , prometheus . GaugeValue , msg . Attributes . Name )
// Skip statistics if argument collector.netclass_rtnl.with-stats is false or statistics are unavailable.
if netclassRTNLWithStats == nil || ! * netclassRTNLWithStats || msg . Attributes . Stats64 == nil {
continue
}
pushMetric ( ch , c . getFieldDesc ( "receive_packets_total" ) , "receive_packets_total" , msg . Attributes . Stats64 . RXPackets , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_packets_total" ) , "transmit_packets_total" , msg . Attributes . Stats64 . TXPackets , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_bytes_total" ) , "receive_bytes_total" , msg . Attributes . Stats64 . RXBytes , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_bytes_total" ) , "transmit_bytes_total" , msg . Attributes . Stats64 . TXBytes , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_errors_total" ) , "receive_errors_total" , msg . Attributes . Stats64 . RXErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_errors_total" ) , "transmit_errors_total" , msg . Attributes . Stats64 . TXErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_dropped_total" ) , "receive_dropped_total" , msg . Attributes . Stats64 . RXDropped , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_dropped_total" ) , "transmit_dropped_total" , msg . Attributes . Stats64 . TXDropped , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "multicast_total" ) , "multicast_total" , msg . Attributes . Stats64 . Multicast , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "collisions_total" ) , "collisions_total" , msg . Attributes . Stats64 . Collisions , prometheus . GaugeValue , msg . Attributes . Name )
// Detailed rx_errors.
pushMetric ( ch , c . getFieldDesc ( "receive_length_errors_total" ) , "receive_length_errors_total" , msg . Attributes . Stats64 . RXLengthErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_over_errors_total" ) , "receive_over_errors_total" , msg . Attributes . Stats64 . RXOverErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_crc_errors_total" ) , "receive_crc_errors_total" , msg . Attributes . Stats64 . RXCRCErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_frame_errors_total" ) , "receive_frame_errors_total" , msg . Attributes . Stats64 . RXFrameErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_fifo_errors_total" ) , "receive_fifo_errors_total" , msg . Attributes . Stats64 . RXFIFOErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_missed_errors_total" ) , "receive_missed_errors_total" , msg . Attributes . Stats64 . RXMissedErrors , prometheus . GaugeValue , msg . Attributes . Name )
// Detailed tx_errors.
pushMetric ( ch , c . getFieldDesc ( "transmit_aborted_errors_total" ) , "transmit_aborted_errors_total" , msg . Attributes . Stats64 . TXAbortedErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_carrier_errors_total" ) , "transmit_carrier_errors_total" , msg . Attributes . Stats64 . TXCarrierErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_fifo_errors_total" ) , "transmit_fifo_errors_total" , msg . Attributes . Stats64 . TXFIFOErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_heartbeat_errors_total" ) , "transmit_heartbeat_errors_total" , msg . Attributes . Stats64 . TXHeartbeatErrors , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_window_errors_total" ) , "transmit_window_errors_total" , msg . Attributes . Stats64 . TXWindowErrors , prometheus . GaugeValue , msg . Attributes . Name )
// For cslip, etc.
pushMetric ( ch , c . getFieldDesc ( "receive_compressed_total" ) , "receive_compressed_total" , msg . Attributes . Stats64 . RXCompressed , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "transmit_compressed_total" ) , "transmit_compressed_total" , msg . Attributes . Stats64 . TXCompressed , prometheus . GaugeValue , msg . Attributes . Name )
pushMetric ( ch , c . getFieldDesc ( "receive_nohandler_total" ) , "receive_nohandler_total" , msg . Attributes . Stats64 . RXNoHandler , prometheus . GaugeValue , msg . Attributes . Name )
}
return nil
}
func ( c * netClassCollector ) getNetClassInfoRTNL ( ) ( [ ] rtnetlink . LinkMessage , error ) {
conn , err := rtnetlink . Dial ( nil )
if err != nil {
return nil , err
}
defer conn . Close ( )
lMsgs , err := conn . Link . List ( )
return lMsgs , err
}
func ( c * netClassCollector ) getLinkModes ( ) ( [ ] * ethtool . LinkMode , error ) {
conn , err := ethtool . New ( )
if err != nil {
return nil , err
}
defer conn . Close ( )
lms , err := conn . LinkModes ( )
return lms , err
}
// getSysfsAttributes reads attributes that are absent from netlink but provided
// by sysfs.
func getSysfsAttributes ( links [ ] rtnetlink . LinkMessage ) ( sysfs . NetClass , error ) {
netClass := sysfs . NetClass { }
for _ , msg := range links {
interfaceClass := sysfs . NetClassIface { }
ifName := msg . Attributes . Name
devPath := filepath . Join ( "/sys" , "class" , "net" , ifName )
// These three attributes hold a device-specific lock when
// accessed, not the RTNL lock, so they are much less impactful
// than reading most of the other attributes from sysfs.
for _ , attr := range [ ] string { "addr_assign_type" , "dev_id" , "name_assign_type" } {
if err := sysfs . ParseNetClassAttribute ( devPath , attr , & interfaceClass ) ; err != nil {
return nil , err
}
}
netClass [ ifName ] = interfaceClass
}
return netClass , nil
}