From b0443dc0abc056a28dd2146425925a1bca8f233c Mon Sep 17 00:00:00 2001 From: Brandon Ewing Date: Thu, 30 Jul 2026 08:53:41 -0500 Subject: [PATCH 1/2] collector/netclass: add master and VRF info metrics The netlink netclass path already receives IFLA_MASTER and IFLA_LINKINFO in the RTM_GETLINK dump it makes, but exposed neither the master device nor its kind, so there is no way to tell which VRF an interface is in. Add node_network_master_info, labelled with the direct master and the master's kind as reported by IFLA_INFO_SLAVE_KIND, covering VRF membership as well as bridge ports and bond members. Also add node_network_vrf_info to publish the routing table ID of VRF devices. rtnetlink has no vrf driver, so IFLA_VRF_TABLE is decoded from the raw IFLA_INFO_DATA payload. Signed-off-by: Brandon Ewing --- collector/netclass_rtnl_linux.go | 65 ++++++++++++++++++++++++ collector/netclass_rtnl_linux_test.go | 71 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 collector/netclass_rtnl_linux_test.go diff --git a/collector/netclass_rtnl_linux.go b/collector/netclass_rtnl_linux.go index c71957cc29..81c20fde65 100644 --- a/collector/netclass_rtnl_linux.go +++ b/collector/netclass_rtnl_linux.go @@ -20,13 +20,16 @@ import ( "fmt" "io/fs" "path/filepath" + "strconv" "strings" "github.com/alecthomas/kingpin/v2" "github.com/jsimonetti/rtnetlink/v2" "github.com/mdlayher/ethtool" + "github.com/mdlayher/netlink" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/procfs/sysfs" + "golang.org/x/sys/unix" ) var ( @@ -35,6 +38,19 @@ var ( "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up", } + + netclassMasterDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "network", "master_info"), + "Direct master device of and the master's kind (bridge, bond, vrf), value is always 1.", + []string{"device", "master", "master_kind"}, + nil, + ) + netclassVRFDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "network", "vrf_info"), + "Routing table ID of a VRF device, value is always 1.", + []string{"device", "table"}, + nil, + ) ) func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) error { @@ -64,8 +80,13 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro return fmt.Errorf("could not get net class info: %w", err) } + // ifNames maps every interface index to its name, so that master devices + // can be resolved by name. Ignored devices are kept, because an ignored + // device may be the master of a device that is not ignored. + ifNames := make(map[uint32]string, len(lMsgs)) relevantLinks := make([]rtnetlink.LinkMessage, 0, len(lMsgs)) for _, msg := range lMsgs { + ifNames[msg.Index] = msg.Attributes.Name if !c.ignoredDevicesPattern.MatchString(msg.Attributes.Name) { relevantLinks = append(relevantLinks, msg) } @@ -128,6 +149,32 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro ch <- prometheus.MustNewConstMetric(altnameDesc, prometheus.GaugeValue, infoValue, strings.ToValidUTF8(altname, "\uFFFD"), msg.Attributes.Name) } } + + // Only the direct master is reported. An interface enslaved to a + // bridge that is itself enslaved to a VRF has master_kind="bridge". + if msg.Attributes.Master != nil { + if master, ok := ifNames[*msg.Attributes.Master]; ok { + // IFLA_INFO_SLAVE_KIND holds the master's kind as + // reported by the kernel. It may be absent, in which + // case the master is still reported with an empty kind. + masterKind := "" + if msg.Attributes.Info != nil { + masterKind = msg.Attributes.Info.SlaveKind + } + ch <- prometheus.MustNewConstMetric(netclassMasterDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, master, masterKind) + } + } + + if msg.Attributes.Info != nil && msg.Attributes.Info.Kind == "vrf" { + // rtnetlink has no vrf driver, so IFLA_INFO_DATA is left as + // raw nested attributes. + if data, ok := msg.Attributes.Info.Data.(*rtnetlink.LinkData); ok { + if table, ok := vrfTable(data.Data); ok { + ch <- prometheus.MustNewConstMetric(netclassVRFDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, strconv.FormatUint(uint64(table), 10)) + } + } + } + pushMetric(ch, c.getFieldDesc("address_assign_type"), ifaceInfo.AddrAssignType, prometheus.GaugeValue, msg.Attributes.Name) pushMetric(ch, c.getFieldDesc("carrier"), msg.Attributes.Carrier, prometheus.GaugeValue, msg.Attributes.Name) pushMetric(ch, c.getFieldDesc("carrier_changes_total"), msg.Attributes.CarrierChanges, prometheus.CounterValue, msg.Attributes.Name) @@ -220,6 +267,24 @@ func (c *netClassCollector) getLinkModes() ([]*ethtool.LinkMode, error) { return lms, err } +// vrfTable decodes the routing table ID from the raw IFLA_INFO_DATA payload of +// a vrf link. It reports false if the attribute is absent or malformed. +func vrfTable(data []byte) (uint32, bool) { + // The attribute decoder defaults to native byte order, which is what + // rtnetlink uses. + ad, err := netlink.NewAttributeDecoder(data) + if err != nil { + return 0, false + } + for ad.Next() { + if ad.Type() == unix.IFLA_VRF_TABLE { + table := ad.Uint32() + return table, ad.Err() == nil + } + } + return 0, false +} + // getSysfsAttributes reads attributes that are absent from netlink but provided // by sysfs. func getSysfsAttributes(links []rtnetlink.LinkMessage) (sysfs.NetClass, error) { diff --git a/collector/netclass_rtnl_linux_test.go b/collector/netclass_rtnl_linux_test.go new file mode 100644 index 0000000000..20c12e21e9 --- /dev/null +++ b/collector/netclass_rtnl_linux_test.go @@ -0,0 +1,71 @@ +// 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 !nonetclass && linux + +package collector + +import "testing" + +func TestVRFTable(t *testing.T) { + tests := []struct { + name string + data []byte + want uint32 + ok bool + }{ + { + // IFLA_INFO_DATA of a vrf device using table 100, as + // returned by RTM_GETLINK. + name: "table 100", + data: []byte{0x08, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x00}, + want: 100, + ok: true, + }, + { + // Table IDs above 255 exercise the multi-byte decode. + name: "table 1000", + data: []byte{0x08, 0x00, 0x01, 0x00, 0xe8, 0x03, 0x00, 0x00}, + want: 1000, + ok: true, + }, + { + name: "no attributes", + data: nil, + ok: false, + }, + { + // IFLA_VRF_TABLE absent, only an unrelated attribute. + name: "missing table attribute", + data: []byte{0x08, 0x00, 0x02, 0x00, 0x64, 0x00, 0x00, 0x00}, + ok: false, + }, + { + name: "truncated attribute", + data: []byte{0x08, 0x00, 0x01, 0x00, 0x64}, + ok: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, ok := vrfTable(test.data) + if ok != test.ok { + t.Fatalf("vrfTable() ok = %v, want %v", ok, test.ok) + } + if ok && got != test.want { + t.Errorf("vrfTable() = %d, want %d", got, test.want) + } + }) + } +} From a665b5175296324e844d78f567ee2a7556659e3c Mon Sep 17 00:00:00 2001 From: Brandon Ewing Date: Thu, 30 Jul 2026 09:59:48 -0500 Subject: [PATCH 2/2] collector/netclass: Add changelog entry for VRF Signed-off-by: Brandon Ewing --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6040344b..9f17faece9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## master / unreleased +* [FEATURE] netlink: Add node_network_master_info and node_network_vrf_info metrics #3765 + ## 1.12.1 / 2026-07-14 * [BUGFIX] perf: Fix perf profiler flag handling #3731