Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ hwmon | chip | --collector.hwmon.chip-include | --collector.hwmon.chip-exclude
hwmon | sensor | --collector.hwmon.sensor-include | --collector.hwmon.sensor-exclude
interrupts | name | --collector.interrupts.name-include | --collector.interrupts.name-exclude
netdev | device | --collector.netdev.device-include | --collector.netdev.device-exclude
netvf | device | --collector.netvf.device-include | --collector.netvf.device-exclude
qdisk | device | --collector.qdisk.device-include | --collector.qdisk.device-exclude
slabinfo | slab-names | --collector.slabinfo.slabs-include | --collector.slabinfo.slabs-exclude
sysctl | all | --collector.sysctl.include | N/A
Expand Down Expand Up @@ -202,6 +203,7 @@ logind | Exposes session counts from [logind](http://www.freedesktop.org/wiki/So
meminfo\_numa | Exposes memory statistics from `/sys/devices/system/node/node[0-9]*/meminfo`, `/sys/devices/system/node/node[0-9]*/numastat`. | Linux
mountstats | Exposes filesystem statistics from `/proc/self/mountstats`. Exposes detailed NFS client statistics. | Linux
network_route | Exposes the routing table as metrics | Linux
netvf | Exposes SR-IOV Virtual Function statistics and configuration from netlink. | Linux
pcidevice | Exposes pci devices' information including their link status and parent devices. | Linux
perf | Exposes perf based metrics (Warning: Metrics are dependent on kernel configuration and settings). | Linux
processes | Exposes aggregate process statistics from `/proc`. | Linux
Expand Down
278 changes: 278 additions & 0 deletions collector/netvf_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright 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 !nonetvf

package collector

import (
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"

"github.com/alecthomas/kingpin/v2"
"github.com/jsimonetti/rtnetlink/v2"
"github.com/prometheus/client_golang/prometheus"
)

const netvfSubsystem = "net_vf"

var (
netvfDeviceInclude = kingpin.Flag("collector.netvf.device-include", "Regexp of PF devices to include (mutually exclusive to device-exclude).").String()
netvfDeviceExclude = kingpin.Flag("collector.netvf.device-exclude", "Regexp of PF devices to exclude (mutually exclusive to device-include).").String()
)

func init() {
registerCollector("netvf", defaultDisabled, NewNetVFCollector)
}

type netvfCollector struct {
logger *slog.Logger
deviceFilter deviceFilter

info *prometheus.Desc
receivePackets *prometheus.Desc
transmitPackets *prometheus.Desc
receiveBytes *prometheus.Desc
transmitBytes *prometheus.Desc
broadcast *prometheus.Desc
multicast *prometheus.Desc
receiveDropped *prometheus.Desc
transmitDropped *prometheus.Desc
}

func NewNetVFCollector(logger *slog.Logger) (Collector, error) {
if *netvfDeviceExclude != "" && *netvfDeviceInclude != "" {
return nil, errors.New("device-exclude & device-include are mutually exclusive")
}

if *netvfDeviceExclude != "" {
logger.Info("Parsed flag --collector.netvf.device-exclude", "flag", *netvfDeviceExclude)
}

if *netvfDeviceInclude != "" {
logger.Info("Parsed flag --collector.netvf.device-include", "flag", *netvfDeviceInclude)
}

return &netvfCollector{
logger: logger,
deviceFilter: newDeviceFilter(*netvfDeviceExclude, *netvfDeviceInclude),
info: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "info"),
"Virtual Function configuration information.",
[]string{"device", "vf", "mac", "vlan", "link_state", "spoof_check", "trust", "pci_address"}, nil,
),
receivePackets: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "receive_packets_total"),
"Number of received packets by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
transmitPackets: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_packets_total"),
"Number of transmitted packets by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
receiveBytes: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "receive_bytes_total"),
"Number of received bytes by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
transmitBytes: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_bytes_total"),
"Number of transmitted bytes by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
broadcast: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "broadcast_packets_total"),
"Number of broadcast packets received by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
multicast: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "multicast_packets_total"),
"Number of multicast packets received by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
receiveDropped: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "receive_dropped_total"),
"Number of dropped received packets by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
transmitDropped: prometheus.NewDesc(
prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_dropped_total"),
"Number of dropped transmitted packets by the VF.",
[]string{"device", "vf", "pci_address"}, nil,
),
}, nil
}

func (c *netvfCollector) Update(ch chan<- prometheus.Metric) error {
conn, err := rtnetlink.Dial(nil)
if err != nil {
return fmt.Errorf("failed to connect to rtnetlink: %w", err)
}
defer conn.Close()

links, err := conn.Link.ListWithVFInfo()
if err != nil {
return fmt.Errorf("failed to list interfaces with VF info: %w", err)
}

vfCount := 0
for _, link := range links {
if link.Attributes == nil {
continue
}

// Skip interfaces without VFs
if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 {
continue
}

device := link.Attributes.Name

// Apply device filter
if c.deviceFilter.ignored(device) {
c.logger.Debug("Ignoring device", "device", device)
continue
}

for _, vf := range link.Attributes.VFInfoList {
vfID := fmt.Sprintf("%d", vf.ID)

// Emit info metric with VF configuration
mac := ""
if vf.MAC != nil {
mac = vf.MAC.String()
}
vlan := fmt.Sprintf("%d", vf.Vlan)
linkState := vfLinkStateString(vf.LinkState)
spoofCheck := fmt.Sprintf("%t", vf.SpoofCheck)
trust := fmt.Sprintf("%t", vf.Trust)
pciAddress := resolveVFPCIAddress(sysFilePath("class"), device, vf.ID)

ch <- prometheus.MustNewConstMetric(c.info, prometheus.GaugeValue, 1, device, vfID, mac, vlan, linkState, spoofCheck, trust, pciAddress)

// Emit stats metrics if available
if vf.Stats == nil {
c.logger.Debug("VF has no stats", "device", device, "vf", vf.ID)
vfCount++
continue
}

stats := vf.Stats

ch <- prometheus.MustNewConstMetric(c.receivePackets, prometheus.CounterValue, float64(stats.RxPackets), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.transmitPackets, prometheus.CounterValue, float64(stats.TxPackets), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.receiveBytes, prometheus.CounterValue, float64(stats.RxBytes), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.transmitBytes, prometheus.CounterValue, float64(stats.TxBytes), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.broadcast, prometheus.CounterValue, float64(stats.Broadcast), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.multicast, prometheus.CounterValue, float64(stats.Multicast), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.receiveDropped, prometheus.CounterValue, float64(stats.RxDropped), device, vfID, pciAddress)
ch <- prometheus.MustNewConstMetric(c.transmitDropped, prometheus.CounterValue, float64(stats.TxDropped), device, vfID, pciAddress)

vfCount++
}
}

if vfCount == 0 {
return ErrNoData
}

return nil
}

func vfLinkStateString(state rtnetlink.VFLinkState) string {
switch state {
case rtnetlink.VFLinkStateAuto:
return "auto"
case rtnetlink.VFLinkStateEnable:
return "enable"
case rtnetlink.VFLinkStateDisable:
return "disable"
default:
return "unknown"
}
}

// resolveVFPCIAddress resolves the PCI BDF address of a VF by reading the
// sysfs virtfn symlink: <sysClassPath>/net/<pfDevice>/device/virtfn<vfID>.
// Returns empty string if the symlink doesn't exist or can't be resolved.
func resolveVFPCIAddress(sysClassPath, pfDevice string, vfID uint32) string {
virtfnPath := filepath.Join(sysClassPath, "net", pfDevice, "device", fmt.Sprintf("virtfn%d", vfID))
resolved, err := os.Readlink(virtfnPath)
if err != nil {
return ""
}
return filepath.Base(resolved)
}

// vfMetrics holds parsed VF metrics for a single VF
type vfMetrics struct {
Device string
VFID uint32
MAC string
Vlan uint32
LinkState string
SpoofCheck bool
Trust bool
PCIAddress string
Stats *rtnetlink.VFStats
}

// parseVFInfo extracts VF information from link messages for testing.
// sysClassPath is the path to the sysfs class directory used to resolve VF PCI addresses.
func parseVFInfo(links []rtnetlink.LinkMessage, filter *deviceFilter, logger *slog.Logger, sysClassPath string) []vfMetrics {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere expect the tests?

Also all these function ideally should go into procfs if needed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, resolveVFPCIAddress is the sole function that reads sysfs — everything else goes through rtnetlink.

For parseVFInfo: I'll keep it as a test utility since it allows unit testing the VF parsing logic without a live netlink socket. WDYT ?

For resolveVFPCIAddress: agreed it would be a natural fit in prometheus/procfs. I can open a follow-up issue/PR there to contribute it upstream — would you prefer that happens before merging this, or as a separate follow-up?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah lets move before merging.

If parseVFInfo is only used in tests, you can also define it there

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, PR in procfs is sent: prometheus/procfs#791

var result []vfMetrics

for _, link := range links {
if link.Attributes == nil {
continue
}

// Skip interfaces without VFs
if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 {
continue
}

device := link.Attributes.Name

// Apply device filter
if filter.ignored(device) {
logger.Debug("Ignoring device", "device", device)
continue
}

for _, vf := range link.Attributes.VFInfoList {
mac := ""
if vf.MAC != nil {
mac = vf.MAC.String()
}

result = append(result, vfMetrics{
Device: device,
VFID: vf.ID,
MAC: mac,
Vlan: vf.Vlan,
LinkState: vfLinkStateString(vf.LinkState),
SpoofCheck: vf.SpoofCheck,
Trust: vf.Trust,
PCIAddress: resolveVFPCIAddress(sysClassPath, device, vf.ID),
Stats: vf.Stats,
})
}
}

return result
}
Loading