diff --git a/README.md b/README.md index 8abf2cdf0f..4d7a191a4c 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ hwmon | sensor | --collector.hwmon.sensor-include | --collector.hwmon.sensor-exc infiniband | device | --collector.infiniband.device-include | --collector.infiniband.device-exclude interrupts | name | --collector.interrupts.name-include | --collector.interrupts.name-exclude netdev | device | --collector.netdev.device-include | --collector.netdev.device-exclude +ndisc | device | --collector.ndisc.device-include | --collector.ndisc.device-exclude qdisc | device | --collector.qdisc.device-include | --collector.qdisc.device-exclude slabinfo | slab-names | --collector.slabinfo.slabs-include | --collector.slabinfo.slabs-exclude sysctl | all | --collector.sysctl.include | N/A @@ -150,6 +151,7 @@ netisr | Exposes netisr statistics | FreeBSD netstat | Exposes network statistics from `/proc/net/netstat`. This is the same information as `netstat -s`. | Linux nfs | Exposes NFS client statistics from `/proc/net/rpc/nfs`. This is the same information as `nfsstat -c`. | Linux nfsd | Exposes NFS kernel server statistics from `/proc/net/rpc/nfsd`. This is the same information as `nfsstat -s`. | Linux +ndisc | Exposes NDISC neighbor statistics. | Linux nvme | Exposes NVMe info from `/sys/class/nvme/` | Linux os | Expose OS release info from `/etc/os-release` or `/usr/lib/os-release` | _any_ powersupplyclass | Exposes Power Supply statistics from `/sys/class/power_supply` | Linux diff --git a/collector/ndisc_linux.go b/collector/ndisc_linux.go new file mode 100644 index 0000000000..0d0daa3664 --- /dev/null +++ b/collector/ndisc_linux.go @@ -0,0 +1,101 @@ +// Copyright 2026 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 !nondisc + +package collector + +import ( + "fmt" + "log/slog" + + "github.com/alecthomas/kingpin/v2" + "github.com/jsimonetti/rtnetlink/v2/rtnl" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sys/unix" +) + +var ( + ndiscDeviceInclude = kingpin.Flag("collector.ndisc.device-include", "Regexp of ndisc devices to include (mutually exclusive to device-exclude).").String() + ndiscDeviceExclude = kingpin.Flag("collector.ndisc.device-exclude", "Regexp of ndisc devices to exclude (mutually exclusive to device-include).").String() +) + +type ndiscCollector struct { + deviceFilter deviceFilter + logger *slog.Logger +} + +func init() { + registerCollector("ndisc", defaultEnabled, NewNdiscCollector) +} + +var ( + ndiscEntries = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "ndisc", "entries"), + "NDISC entries by device", + []string{"device"}, nil, + ) +) + +// NewNdiscCollector returns a new Collector exposing NDISC stats. +func NewNdiscCollector(logger *slog.Logger) (Collector, error) { + return &ndiscCollector{ + deviceFilter: newDeviceFilter(*ndiscDeviceExclude, *ndiscDeviceInclude), + logger: logger, + }, nil +} + +func getTotalNdiscEntries(neighbors []*rtnl.Neigh) map[string]uint32 { + entries := make(map[string]uint32) + + for _, n := range neighbors { + if n.State&unix.NUD_NOARP == 0 && n.Interface != nil { + entries[n.Interface.Name]++ + } + } + + return entries +} + +func getTotalNdiscEntriesRTNL() (map[string]uint32, error) { + conn, err := rtnl.Dial(nil) + if err != nil { + return nil, err + } + defer conn.Close() + + neighbors, err := conn.Neighbours(nil, unix.AF_INET6) + if err != nil { + return nil, err + } + + return getTotalNdiscEntries(neighbors), nil +} + +func (c *ndiscCollector) Update(ch chan<- prometheus.Metric) error { + enumeratedEntries, err := getTotalNdiscEntriesRTNL() + if err != nil { + return fmt.Errorf("could not get NDISC entries: %w", err) + } + + for device, entryCount := range enumeratedEntries { + if c.deviceFilter.ignored(device) { + continue + } + ch <- prometheus.MustNewConstMetric( + ndiscEntries, prometheus.GaugeValue, float64(entryCount), device, + ) + } + + return nil +} diff --git a/collector/ndisc_linux_test.go b/collector/ndisc_linux_test.go new file mode 100644 index 0000000000..2cf9c9f1cd --- /dev/null +++ b/collector/ndisc_linux_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 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 !nondisc + +package collector + +import ( + "net" + "testing" + + "github.com/jsimonetti/rtnetlink/v2/rtnl" + "golang.org/x/sys/unix" +) + +func TestGetTotalNdiscEntries(t *testing.T) { + t.Parallel() + + neighbors := []*rtnl.Neigh{ + {Interface: &net.Interface{Name: "eth0"}, State: unix.NUD_REACHABLE}, + {Interface: &net.Interface{Name: "eth0"}, State: unix.NUD_STALE}, + {Interface: &net.Interface{Name: "eth1"}, State: unix.NUD_DELAY}, + {Interface: &net.Interface{Name: "eth1"}, State: unix.NUD_NOARP}, + } + + entries := getTotalNdiscEntries(neighbors) + + if got, want := entries["eth0"], uint32(2); got != want { + t.Fatalf("unexpected entry count for eth0: got %d, want %d", got, want) + } + if got, want := entries["eth1"], uint32(1); got != want { + t.Fatalf("unexpected entry count for eth1: got %d, want %d", got, want) + } +}