Skip to content
Open
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 @@ -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
Expand Down Expand Up @@ -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
Expand Down
101 changes: 101 additions & 0 deletions collector/ndisc_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
44 changes: 44 additions & 0 deletions collector/ndisc_linux_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}