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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Flags:
--[no-]collector.route Enable the route collector (default: enabled, to disable use
--no-collector.route).
--[no-]collector.vrrp Enable the vrrp collector (default: disabled).
--[no-]collector.mpls_ldp Enable the mpls_ldp collector (default: disabled).
--[no-]collector.mpls_ldp.binding-in-use
Enable the frr_mpls_ldp_binding_in_use metric (default: disabled).
--web.telemetry-path="/metrics"
Path under which to expose metrics.
--web.listen-address=:9342 ...
Expand Down Expand Up @@ -136,6 +139,7 @@ BGP IPv6 | Per VRF and address family (currently support unicast only) BGP IPv6
BGP L2VPN | Per VRF and address family (currently support EVPN only) BGP L2VPN EVPN metrics:<br> - RIB entries<br> - RIB memory usage<br> - Configured peer count<br> - Peer memory usage<br> - Configure peer group count<br> - Peer group memory usage<br> - Peer messages in<br> - Peer messages out<br> - Peer active prfixes<br> - Peer state (established/down)<br> - Peer uptime
VRRP | Per VRRP Interface, VrID and Protocol:<br> - Rx and TX statistics<br> - VRRP Status<br> - VRRP State Transitions<br>
PIM | PIM metrics:<br> - Neighbor count<br> - Neighbor uptime
MPLS LDP | MPLS LDP metrics:<br> - Binding count<br> - Binding in use (if enabled)<br> - IGP Sync state<br> - Interface state<br> - Interface hello interval<br> - Interface hello holdtime<br> - Interface adjacency count<br> - Neighbor state<br> - Neighbor uptime<br> - Discovery adjacency count

### Sending commands to FRR

Expand Down
7 changes: 7 additions & 0 deletions collector/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ func executeVRRPCommand(cmd string) ([]byte, error) {
return socketConn.ExecVRRPCmd(cmd)
}

func executeMPLSLDPCommand(cmd string) ([]byte, error) {
if *vtyshEnable {
return execVtyshCommand(cmd)
}
return socketConn.ExecLDPDCmd(cmd)
}

func execVtyshCommand(vtyshCmd string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), *vtyshTimeout)
defer cancel()
Expand Down
326 changes: 326 additions & 0 deletions collector/mpls_ldp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
package collector

import (
"encoding/json"
"fmt"
"log/slog"
"strings"

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

var (
mplsLdpSubsystem = "mpls_ldp"

mplsLDPBindingInUse = kingpin.Flag("collector.mpls_ldp.binding-in-use", "Enable the frr_mpls_ldp_binding_in_use metric (default: disabled).").Default("False").Bool()
)

func init() {
registerCollector(mplsLdpSubsystem, disabledByDefault, NewMPLSLDPCollector)
}

type mplsLDPCollector struct {
logger *slog.Logger
descriptions map[string]*prometheus.Desc
}

func NewMPLSLDPCollector(logger *slog.Logger) (Collector, error) {
return &mplsLDPCollector{logger: logger, descriptions: getMPLSLDPDesc()}, nil
}

func getMPLSLDPDesc() map[string]*prometheus.Desc {
bindingsLabels := []string{"address_family"}
bindingLabels := []string{"address_family", "prefix", "neighbor_id", "local_label", "remote_label"}
igpSyncLabels := []string{"interface", "peer_ldp_id"}
interfaceLabels := []string{"name", "address_family"}
neighborLabels := []string{"address_family", "neighbor_id"}
discoveryLabels := []string{"address_family", "neighbor_id", "interface", "type"}

return map[string]*prometheus.Desc{
"bindingCount": colPromDesc(mplsLdpSubsystem, "binding_count", "Number of MPLS LDP bindings.", bindingsLabels),
"bindingInUse": colPromDesc(mplsLdpSubsystem, "binding_in_use", "Usage status of the MPLS LDP binding (1=In Use, 0=Not In Use).", bindingLabels),
"igpSyncState": colPromDesc(mplsLdpSubsystem, "igp_sync_state", "State of MPLS LDP IGP sync (1=Ready/Complete, 0=Not Complete).", igpSyncLabels),
"interfaceState": colPromDesc(mplsLdpSubsystem, "interface_state", "State of MPLS LDP interface (1=Active, 0=Inactive).", interfaceLabels),
"interfaceHelloInterval": colPromDesc(mplsLdpSubsystem, "interface_hello_interval_seconds", "Hello interval for the interface.", interfaceLabels),
"interfaceHelloHoldtime": colPromDesc(mplsLdpSubsystem, "interface_hello_holdtime_seconds", "Hello holdtime for the interface.", interfaceLabels),
"interfaceAdjacencyCount": colPromDesc(mplsLdpSubsystem, "interface_adjacency_count", "Number of adjacencies on the interface.", interfaceLabels),
"neighborState": colPromDesc(mplsLdpSubsystem, "neighbor_state", "State of MPLS LDP neighbor (1=Operational, 0=Other).", neighborLabels),
"neighborUptime": colPromDesc(mplsLdpSubsystem, "neighbor_uptime_seconds", "Uptime of MPLS LDP neighbor in seconds.", neighborLabels),
"discoveryAdjacencyCount": colPromDesc(mplsLdpSubsystem, "discovery_adjacency_count", "Number of discovery adjacencies.", discoveryLabels),
}
}

func (c *mplsLDPCollector) Update(ch chan<- prometheus.Metric) error {
// 1. Bindings
if err := c.collectBindings(ch); err != nil {
return err
}
// 2. IGP Sync
if err := c.collectIGPSync(ch); err != nil {
return err
}
// 3. Interface
if err := c.collectInterface(ch); err != nil {
return err
}
// 4. Neighbor
if err := c.collectNeighbor(ch); err != nil {
return err
}
// 5. Discovery
if err := c.collectDiscovery(ch); err != nil {
return err
}
return nil
}

// ----------------------------------------------------------------------
// Bindings
// ----------------------------------------------------------------------

type mplsLdpBindings struct {
Bindings []struct {
AddressFamily string `json:"addressFamily"`
Prefix string `json:"prefix"`
NeighborID string `json:"neighborId"`
LocalLabel string `json:"localLabel"`
RemoteLabel string `json:"remoteLabel"`
InUse int `json:"inUse"`
} `json:"bindings"`
}

func (c *mplsLDPCollector) collectBindings(ch chan<- prometheus.Metric) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like collectMPLSLDPBindings so there is no conflict with other collectors?

cmd := "show mpls ldp binding json"
output, err := executeMPLSLDPCommand(cmd)
if err != nil {
return err
}
if err := processBindings(ch, output, c.descriptions); err != nil {
return cmdOutputProcessError(cmd, string(output), err)
}
return nil
}

func processBindings(ch chan<- prometheus.Metric, output []byte, descs map[string]*prometheus.Desc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like processMPLSLDPBindings so there is no conflict with other collectors?

var data mplsLdpBindings
if err := json.Unmarshal(output, &data); err != nil {
return fmt.Errorf("cannot unmarshal mpls ldp binding json: %w", err)
}

counts := make(map[string]float64)
for _, b := range data.Bindings {
counts[b.AddressFamily]++
if *mplsLDPBindingInUse {
newGauge(ch, descs["bindingInUse"], float64(b.InUse), b.AddressFamily, b.Prefix, b.NeighborID, b.LocalLabel, b.RemoteLabel)
}
}

for af, count := range counts {
newGauge(ch, descs["bindingCount"], count, af)
}
return nil
}

// ----------------------------------------------------------------------
// IGP Sync
// ----------------------------------------------------------------------

type mplsLdpIGPSync struct {
State string `json:"state"`
WaitTime int `json:"waitTime"`
PeerLdpID string `json:"peerLdpId"`
}

type mplsLdpIGPSyncOutput map[string]mplsLdpIGPSync

func (c *mplsLDPCollector) collectIGPSync(ch chan<- prometheus.Metric) error {
cmd := "show mpls ldp igp-sync json"
output, err := executeMPLSLDPCommand(cmd)
if err != nil {
return err
}
if err := processIGPSync(ch, output, c.descriptions); err != nil {
return cmdOutputProcessError(cmd, string(output), err)
}
return nil
}

func processIGPSync(ch chan<- prometheus.Metric, output []byte, descs map[string]*prometheus.Desc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like processMPLSLDPIGMPSync so there is no conflict with other collectors?

var data mplsLdpIGPSyncOutput
if err := json.Unmarshal(output, &data); err != nil {
return fmt.Errorf("cannot unmarshal mpls ldp igp-sync json: %w", err)
}

for iface, info := range data {
stateVal := 0.0
if !strings.Contains(strings.ToLower(info.State), "notcomplete") {
stateVal = 1.0
}
newGauge(ch, descs["igpSyncState"], stateVal, iface, info.PeerLdpID)
}
return nil
}

// ----------------------------------------------------------------------
// Interface
// ----------------------------------------------------------------------

type mplsLdpInterface struct {
Name string `json:"name"`
AddressFamily string `json:"addressFamily"`
State string `json:"state"`
UpTime string `json:"upTime"`
HelloInterval float64 `json:"helloInterval"`
HelloHoldtime float64 `json:"helloHoldtime"`
AdjacencyCount float64 `json:"adjacencyCount"`
}

type mplsLdpInterfaceOutput map[string]mplsLdpInterface

func (c *mplsLDPCollector) collectInterface(ch chan<- prometheus.Metric) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like collectMPLSLDPInterface so there is no conflict with other collectors?

cmd := "show mpls ldp interface json"
output, err := executeMPLSLDPCommand(cmd)
if err != nil {
return err
}
if err := processInterface(ch, output, c.descriptions); err != nil {
return cmdOutputProcessError(cmd, string(output), err)
}
return nil
}

func processInterface(ch chan<- prometheus.Metric, output []byte, descs map[string]*prometheus.Desc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like processMPLSLDPInterface so there is no conflict with other collectors?

var data mplsLdpInterfaceOutput
if err := json.Unmarshal(output, &data); err != nil {
return fmt.Errorf("cannot unmarshal mpls ldp interface json: %w", err)
}

for _, info := range data {
stateVal := 0.0
if strings.EqualFold(info.State, "active") {
stateVal = 1.0
}
newGauge(ch, descs["interfaceState"], stateVal, info.Name, info.AddressFamily)
newGauge(ch, descs["interfaceHelloInterval"], info.HelloInterval, info.Name, info.AddressFamily)
newGauge(ch, descs["interfaceHelloHoldtime"], info.HelloHoldtime, info.Name, info.AddressFamily)
newGauge(ch, descs["interfaceAdjacencyCount"], info.AdjacencyCount, info.Name, info.AddressFamily)
}
return nil
}

// ----------------------------------------------------------------------
// Neighbor
// ----------------------------------------------------------------------

type mplsLdpNeighbor struct {
AddressFamily string `json:"addressFamily"`
NeighborID string `json:"neighborId"`
State string `json:"state"`
UpTime string `json:"upTime"`
}

type mplsLdpNeighborOutput struct {
Neighbors []mplsLdpNeighbor `json:"neighbors"`
}

func (c *mplsLDPCollector) collectNeighbor(ch chan<- prometheus.Metric) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like collectMPLSLDPNeighbor so there is no conflict with other collectors?

cmd := "show mpls ldp neighbor json"
output, err := executeMPLSLDPCommand(cmd)
if err != nil {
return err
}
if err := processNeighbor(ch, output, c.descriptions); err != nil {
return cmdOutputProcessError(cmd, string(output), err)
}
return nil
}

func processNeighbor(ch chan<- prometheus.Metric, output []byte, descs map[string]*prometheus.Desc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like processMPLSLDPNeighbor so there is no conflict with other collectors?

var data mplsLdpNeighborOutput
if err := json.Unmarshal(output, &data); err != nil {
return fmt.Errorf("cannot unmarshal mpls ldp neighbor json: %w", err)
}

for _, n := range data.Neighbors {
stateVal := 0.0
if strings.EqualFold(n.State, "operational") {
stateVal = 1.0
}
newGauge(ch, descs["neighborState"], stateVal, n.AddressFamily, n.NeighborID)

uptimeSeconds, err := parseUptime(n.UpTime)
if err == nil {
newGauge(ch, descs["neighborUptime"], uptimeSeconds, n.AddressFamily, n.NeighborID)
}
}
return nil
}

// ----------------------------------------------------------------------
// Discovery
// ----------------------------------------------------------------------

type mplsLdpDiscovery struct {
AddressFamily string `json:"addressFamily"`
NeighborID string `json:"neighborId"`
Type string `json:"type"`
Interface string `json:"interface"`
}

type mplsLdpDiscoveryOutput struct {
Adjacencies []mplsLdpDiscovery `json:"adjacencies"`
}

func (c *mplsLDPCollector) collectDiscovery(ch chan<- prometheus.Metric) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like collectMPLSLDPDiscovery so there is no conflict with other collectors?

cmd := "show mpls ldp discovery json"
output, err := executeMPLSLDPCommand(cmd)
if err != nil {
return err
}
if err := processDiscovery(ch, output, c.descriptions); err != nil {
return cmdOutputProcessError(cmd, string(output), err)
}
return nil
}

func processDiscovery(ch chan<- prometheus.Metric, output []byte, descs map[string]*prometheus.Desc) error {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you update this function to something like processMPLSLDPDiscovery so there is no conflict with other collectors?

var data mplsLdpDiscoveryOutput
if err := json.Unmarshal(output, &data); err != nil {
return fmt.Errorf("cannot unmarshal mpls ldp discovery json: %w", err)
}

counts := make(map[string]float64)
for _, d := range data.Adjacencies {
key := strings.Join([]string{d.AddressFamily, d.NeighborID, d.Interface, d.Type}, "|")
Copy link
Owner

Choose a reason for hiding this comment

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

Using strings.Join with "|" as separator and then strings.Split to recover part seems risky. Could we use a structured instead? e.g.

type discoveryKey struct {
    AF, Neighbor, Iface, Type string
}

counts[key]++
}

for key, count := range counts {
parts := strings.Split(key, "|")
if len(parts) == 4 {
newGauge(ch, descs["discoveryAdjacencyCount"], count, parts[0], parts[1], parts[2], parts[3])
}
}
return nil
}

// Helper to parse uptime string "HH:MM:SS"
func parseUptime(uptime string) (float64, error) {
Copy link
Owner

Choose a reason for hiding this comment

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

I think it is better to reuse parseHMS from collector/pim.go as it performs the same function. Can you please reuse it, but also move parseHMS to collector/collector.go?

parts := strings.Split(uptime, ":")
if len(parts) == 3 {
h, err1 := parseInt(parts[0])
m, err2 := parseInt(parts[1])
s, err3 := parseInt(parts[2])
if err1 == nil && err2 == nil && err3 == nil {
return float64(h*3600 + m*60 + s), nil
}
}
return 0, fmt.Errorf("invalid uptime format: %s", uptime)
}

func parseInt(s string) (int, error) {
var val int
_, err := fmt.Sscanf(s, "%d", &val)
return val, err
}
Loading
Loading