pax_global_header00006660000000000000000000000064151644422240014516gustar00rootroot0000000000000052 comment=33d47c89c48151286298cc36105c71b2cf109ecb prometheus-frr-exporter-1.11.0/000077500000000000000000000000001516444222400164265ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/.circleci/000077500000000000000000000000001516444222400202615ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/.circleci/config.yml000066400000000000000000000017631516444222400222600ustar00rootroot00000000000000version: 2.1 executors: golang: docker: # Whenever the Go version is updated here, .promu.yml, Dockerfile and line 6 of this file should also be updated. - image: cimg/go:1.25 jobs: test: executor: golang steps: - checkout - run: make test build: executor: golang steps: - checkout - setup_remote_docker - run: make setup_promu - run: ./promu crossbuild - run: ./promu crossbuild tarballs - run: ./promu checksum .tarballs release: executor: golang steps: - checkout - setup_remote_docker - run: make setup_promu - run: ./promu crossbuild - run: ./promu crossbuild tarballs - run: ./promu checksum .tarballs - run: ./promu release .tarballs workflows: version: 2 build_and_release: jobs: - test - build - release: filters: branches: ignore: /.*/ tags: only: /v[0-9]+(\.[0-9]+)*(-.*)*/ prometheus-frr-exporter-1.11.0/.github/000077500000000000000000000000001516444222400177665ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/.github/workflows/000077500000000000000000000000001516444222400220235ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/.github/workflows/docker_upload.yml000066400000000000000000000033031516444222400253600ustar00rootroot00000000000000name: frr_exporter_docker_upload on: push: branches: - master release: types: created jobs: docker: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Check + set version tag run: echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo latest_non_release)" >> $GITHUB_ENV - name: Build and push image uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | tynany/frr_exporter:${{ env.GIT_TAG }} ghcr.io/tynany/frr_exporter:${{ env.GIT_TAG }} # only push latest tag if a release. - name: Build and push image latest tag if: env.GIT_TAG != 'latest_non_release' uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | tynany/frr_exporter:latest ghcr.io/tynany/frr_exporter:latest - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} prometheus-frr-exporter-1.11.0/.github/workflows/golangci-lint.yml000066400000000000000000000005271516444222400253010ustar00rootroot00000000000000name: golangci-lint on: push: pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: "1.25" - uses: actions/checkout@v5 - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: version: v2.6 prometheus-frr-exporter-1.11.0/.gitignore000066400000000000000000000007241516444222400204210ustar00rootroot00000000000000 # Created by https://www.gitignore.io/api/go # Edit at https://www.gitignore.io/?templates=go ### Go ### # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ ### Go Patch ### /vendor/ /Godeps/ frr_exporter # End of https://www.gitignore.io/api/go prometheus-frr-exporter-1.11.0/.golangci.yml000066400000000000000000000010271516444222400210120ustar00rootroot00000000000000version: "2" linters: enable: - misspell - revive - sloglint settings: errcheck: exclude-functions: - (net/http.ResponseWriter).Write exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - gofumpt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ prometheus-frr-exporter-1.11.0/.promu.yml000066400000000000000000000016371516444222400204000ustar00rootroot00000000000000go: # Whenever the Go version is updated here, .circle/config.yml and Dockerfile should also be updated. version: 1.25 repository: path: github.com/tynany/frr_exporter build: binaries: - name: frr_exporter flags: -a -tags 'netgo static_build' ldflags: | -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.Revision={{.Revision}} -X github.com/prometheus/common/version.Branch={{.Branch}} -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} crossbuild: platforms: - linux/amd64 - linux/386 - linux/arm - linux/arm64 - darwin/amd64 - darwin/arm - darwin/arm64 - freebsd/amd64 - freebsd/386 - freebsd/arm - freebsd/arm64 prometheus-frr-exporter-1.11.0/Dockerfile000066400000000000000000000006451516444222400204250ustar00rootroot00000000000000# Whenever the Go version is updated here, .circle/config.yml and .promu.yml should also be updated. FROM golang:1.25 WORKDIR /go/src/github.com/tynany/frr_exporter COPY . /go/src/github.com/tynany/frr_exporter RUN make setup_promu RUN ./promu build RUN ls -lah FROM quay.io/frrouting/frr:10.1.3 WORKDIR /app COPY --from=0 /go/src/github.com/tynany/frr_exporter/frr_exporter . EXPOSE 9342 ENTRYPOINT [ "./frr_exporter"] prometheus-frr-exporter-1.11.0/LICENSE000066400000000000000000000020461516444222400174350ustar00rootroot00000000000000MIT License Copyright (c) 2018 Tynan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prometheus-frr-exporter-1.11.0/Makefile000066400000000000000000000004631516444222400200710ustar00rootroot00000000000000PROMU_VERSION := 0.17.0 setup_promu: curl -s -L https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).linux-amd64.tar.gz | tar -xvzf - mv promu-$(PROMU_VERSION).linux-amd64/promu . build: ./promu build --prefix $(PREFIX) $(PROMU_BINARIES) test: go test ./... prometheus-frr-exporter-1.11.0/README.md000066400000000000000000000367341516444222400177220ustar00rootroot00000000000000# Free Range Routing (FRR) Exporter Prometheus exporter for FRR version 3.0+ that collects metrics from the FRR Unix sockets and exposes them via HTTP, ready for collecting by Prometheus. ## Getting Started To run FRR Exporter: ``` ./frr_exporter [flags] ``` To view metrics on the default port (9342) and path (/metrics): ``` http://device:9342/metrics ``` To view available flags: ``` usage: frr_exporter [] Flags: -h, --[no-]help Show context-sensitive help (also try --help-long and --help-man). --[no-]collector.bgp.peer-types Enable the frr_bgp_peer_types_up metric (default: disabled). --collector.bgp.peer-types.keys=type ... Select the keys from the JSON formatted BGP peer description of which the values will be used with the frr_bgp_peer_types_up metric. Supports multiple values (default: type). --[no-]collector.bgp.peer-descriptions Add the value of the desc key from the JSON formatted BGP peer description as a label to peer metrics. (default: disabled). --[no-]collector.bgp.peer-groups Adds the peer's peer group name as a label. (default: disabled). --[no-]collector.bgp.peer-hostnames Adds the peer's hostname as a label. (default: disabled). --[no-]collector.bgp.peer-descriptions.plain-text Use the full text field of the BGP peer description instead of the value of the JSON formatted desc key (default: disabled). --[no-]collector.bgp.advertised-prefixes Enables the frr_exporter_bgp_prefixes_advertised_count_total metric which exports the number of advertised prefixes to a BGP peer. This is an option for older versions of FRR that don't have PfxSent field (default: disabled). --[no-]collector.bgp.accepted-filtered-prefixes Enable retrieval of accepted and filtered BGP prefix counts (default: disabled). --[no-]collector.bgp.next-hop-interface Adds the peer's next-hop interface label. (default: disabled). --collector.bgp.monitored-prefixes="" Path to a file listing prefixes to monitor for per-peer presence (one per line, # comments allowed). --frr.socket.dir-path="/var/run/frr" Path of of the localstatedir containing each daemon's Unix socket. --frr.socket.timeout=20s Timeout when connecting to the FRR daemon Unix sockets --[no-]frr.vtysh Use vtysh to query FRR instead of each daemon's Unix socket (default: disabled, recommended: disabled). --frr.vtysh.path="/usr/bin/vtysh" Path of vtysh. --frr.vtysh.timeout=20s The timeout when running vtysh commands (default: 20s). --[no-]frr.vtysh.sudo Enable sudo when executing vtysh commands. --frr.vtysh.options="" Additional options passed to vtysh. --collector.ospf.instances="" Comma-separated list of instance IDs if using multiple OSPF instances --[no-]collector.route.detailed-routes Enable detailed route count of each route type (default: disabled). --[no-]collector.bfd Enable the bfd collector (default: enabled, to disable use --no-collector.bfd). --[no-]collector.bgp Enable the bgp collector (default: enabled, to disable use --no-collector.bgp). --[no-]collector.bgp6 Enable the bgp6 collector (default: disabled). --[no-]collector.bgpl2vpn Enable the bgpl2vpn collector (default: disabled). --[no-]collector.ospf Enable the ospf collector (default: enabled, to disable use --no-collector.ospf). --[no-]collector.pim Enable the pim collector (default: disabled). --[no-]collector.route Enable the route collector (default: enabled, to disable use --no-collector.route). --[no-]collector.rpki Enable the rpki collector (default: disabled). --[no-]collector.vrrp Enable the vrrp collector (default: disabled). --web.telemetry-path="/metrics" Path under which to expose metrics. --web.listen-address=:9342 ... Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100` for http, `vsock://:9100` for vsock --web.config.file="" Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md --log.level=info Only log messages with the given severity or above. One of: [debug, info, warn, error] --log.format=logfmt Output format of log messages. One of: [logfmt, json] --[no-]version Show application version. ``` Promethues configuraiton: ``` scrape_configs: - job_name: frr static_configs: - targets: - device1:9342 - device2:9342 relabel_configs: - source_labels: [__address__] regex: "(.*):\d+" target: instance ``` ## Docker A Docker container is available at: - Docker Hub: [tynany/frr_exporter](https://hub.docker.com/r/tynany/frr_exporter) - GitHub Container Registry: [ghcr.io/tynany/frr_exporter](https://github.com/tynany/frr_exporter/pkgs/container/frr_exporter) ### Example Mount the FRR socket directory (default `/var/run/frr`) inside the container, passing that directory to FRR Exporter via the `--frr.socket.dir-path` flag: ``` docker run --restart unless-stopped -d -p 9342:9342 -v /var/run/frr:/frr_sockets tynany/frr_exporter "--frr.socket.dir-path=/frr_sockets" ``` #### If using the --frr.vtysh flag (not recommended) Mount the FRR config directory (default `/etc/frr`) and FRR socket directory (default `/var/run/frr`) inside the container, passing those directories to vtysh options `--vty_socket` & `--config_dir` via the FRR Exporter flag `--frr.vtysh.options` if using: ``` docker run --restart unless-stopped -d -p 9342:9342 -v /etc/frr:/frr_config -v /var/run/frr:/frr_sockets tynany/frr_exporter "--frr.vtysh --frr.vtysh.options=--vty_socket=/frr_sockets --config_dir=/frr_config" ``` ## Collectors To disable a default collector, use the `--no-collector.$name` flag, or `--collector.$name` to enable it. ### Enabled by Default Name | Description --- | --- BGP | Per VRF and address family (currently support unicast only) BGP metrics:
- RIB entries
- RIB memory usage
- Configured peer count
- Peer memory usage
- Configure peer group count
- Peer group memory usage
- Peer messages in
- Peer messages out
- Peer received prefixes
- Peer advertised prefixes
- Peer state (established/down)
- Peer uptime OSPFv4 | Per VRF OSPF metrics:
- Neighbors
- Neighbor adjacencies BFD | BFD Peer metrics:
- Count of total number of peers
- BFD Peer State (up/down)
- BFD Peer Uptime in seconds Route | Route metrics:
- Total number of routes in RIB
- Total number of routes in FIB
- Number of routes of each type (connected/local/ebgp/ospf) in RIB/FIB ### Disabled by Default Name | Description --- | --- BGP IPv6 | Per VRF and address family (currently support unicast only) BGP IPv6 metrics:
- RIB entries
- RIB memory usage
- Configured peer count
- Peer memory usage
- Configure peer group count
- Peer group memory usage
- Peer messages in
- Peer messages out
- Peer active prfixes
- Peer state (established/down)
- Peer uptime BGP L2VPN | Per VRF and address family (currently support EVPN only) BGP L2VPN EVPN metrics:
- RIB entries
- RIB memory usage
- Configured peer count
- Peer memory usage
- Configure peer group count
- Peer group memory usage
- Peer messages in
- Peer messages out
- Peer active prfixes
- Peer state (established/down)
- Peer uptime RPKI | Per VRF RPKI cache-connection metrics (requires FRR compiled with `--enable-rpki`):
- Cache connection state (connected/disconnected)
- Cache connection preference VRRP | Per VRRP Interface, VrID and Protocol:
- Rx and TX statistics
- VRRP Status
- VRRP State Transitions
PIM | PIM metrics:
- Neighbor count
- Neighbor uptime ### Sending commands to FRR By default, FRR Exporter sends commands to FRR via the Unix sockets exposed by each FRR daemon (e.g. bgpd, ospfd, etc), usually located in `/var/run/frr`. If the sockets are located in a folder other than `/var/run/frr`, pass that directory to FRR Exporter via the `--frr.socket.dir-path` flag. #### VTYSH If desired, FRR Exporter can interface with FRR via the `vtysh` command by passing the `--frr.vtysh` flag to FRR Exporter. This is not recommended, and is far slower than FRR Exporter's default way of sending commands to FRR via Unix sockets. The default timeout is 20s but can be modified via the `--frr.vtysh.timeout` flag. ### BGP: Peer Description Labels The description of a BGP peer can be added as a label to all peer metrics by passing the `--collector.bgp.peer-descriptions` flag. The peer description must be JSON formatted with a `desc` field. Example configuration: ``` router bgp 64512 neighbor 192.168.0.1 remote-as 64513 neighbor 192.168.0.1 description {"desc":"important peer"} ``` If an unstructured description is preferred, additionally to `--collector.bgp.peer-descriptions` pass the `--collector.bgp.peer-descriptions.plain-text` flag. Example configuration: ``` router bgp 64512 neighbor 192.168.0.1 remote-as 64513 neighbor 192.168.0.1 description important peer ``` Note, it is recommended to leave this feature disabled as peer descriptions can easily change, resulting in a new time series. ### BGP: Advertised Prefixes to a Peer This is an option for older versions of FRR. If your FRR shows the "PfxSnt" field for Peers in the Established state in the output of `show bgp summary json`, you don't need to enable this option. The number of prefixes advertised to a BGP peer can be enabled (i.e. the `frr_exporter_bgp_prefixes_advertised_count_total` metric) by passing the `--collector.bgp.advertised-prefixes` flag. Please note, older FRR versions do not expose a summary of prefixes advertised to BGP peers, so each peer needs to be queried individually. For example, if 20 BGP peers are configured, 20 'sh ip bgp neigh X.X.X.X advertised-routes json' commands are sent to the Unix socket (or `vtysh` if the `--frr.vtysh` is used). This can be slow, especially if using the `--frr.vtysh` flag. The commands are run in parallel by FRR Exporter, but FRR executes them in serial. Due to the potential negative performance implications of running `vtysh` for every BGP peer, this metric is disabled by default. ### BGP: frr_bgp_peer_types_up FRR Exporter exposes a special metric, `frr_bgp_peer_types_up`, that can be used in scenarios where you want to create Prometheus queries that report on the number of types of BGP peers that are currently established, such as for Alertmanager. To implement this metric, a JSON formatted description must be configured on your BGP group. FRR Exporter will then use the value from the keys specific by the `--collector.bgp.peer-types.keys` flag (the default is `type`), and aggregates all BGP peers that are currently established and configured with that type. For example, if you want to know how many BGP peers are currently established that provide internet, you'd set the description of all BGP groups that provide internet to `{"type":"internet"}` and query Prometheus with `frr_bgp_peer_types_up{type="internet"})`. Going further, if you want to create an alert when the number of established BGP peers that provide internet is 1 or less, you'd use `sum(frr_bgp_peer_types_up{type="internet"}) <= 1`. To enable `frr_bgp_peer_types_up`, use the `--collector.bgp.peer-types` flag. ### BGP: Monitored Prefix Presence FRR Exporter can monitor whether specific prefixes are received from or advertised to each established BGP peer by passing a file path to the `--collector.bgp.monitored-prefixes` flag. This is useful for alerting when an important prefix disappears from a peer's routes. The file should list one prefix per line. Blank lines and lines starting with `#` are ignored: ``` # Critical prefixes to monitor 10.0.0.0/24 10.1.0.0/16 fd00::/48 ``` This produces two metrics per prefix per established peer: ``` frr_bgp_peer_prefix_received{vrf, afi, safi, local_as, peer, peer_as, prefix} 1 or 0 frr_bgp_peer_prefix_advertised{vrf, afi, safi, local_as, peer, peer_as, prefix} 1 or 0 ``` A value of `1` means the prefix is present; `0` means absent. Emitting `0` (rather than omitting the metric) enables `== 0` alerting without `absent()`. The prefix file is read once at startup. A restart is required to pick up changes. Only established peers (ipv4/ipv6) are queried. Note that each established peer requires two additional FRR commands (received routes and advertised routes), so keep the number of monitored prefixes and peers in mind. ### OSPF: Multiple Instance Support [OSPF Mulit-instace](https://docs.frrouting.org/en/latest/ospfd.html#multi-instance-support) is supported by passing a comma-separated list of instances ID to FRR Exporter via the `--collector.ospf.instances` flag. For example, if `/etc/frr/daemons` contains the below configuration, FRR Exporter should be run as: `./frr_exporter --collector.ospf.instances=1,5,6`. ``` ... ospfd=yes ospfd_instances=1,5,6 ... ``` Note: FRR Exporter does not support multi-instance when using `vtysh` to interface with FRR via the `--frr.vtysh` flag for the following reasons: * Invalid JSON is returned when OSPF commands are executed by `vtysh`. For example,\ `show ip ospf vrf all interface json` returns the concatenated JSON from each OSPF instance. * Vtysh does not support `vrf` and `instance` in the same commend. For example,\ `show ip ospf 1 vrf all interface json` is an invalid command. ## Grafana Dashboards A dashboard showing metrics from the BGP collector is included in the repository at [dashboards/grafana-bgp.json](dashboards/grafana-bgp.json), contributed by [Mark Dastmalchi-Round](https://markround.com). It is also published on the Grafana Dashboards catalog at [https://grafana.com/grafana/dashboards/22943-frr-exporter-bgp/](https://grafana.com/grafana/dashboards/22943-frr-exporter-bgp/) (along with a link to a live demo instance) where it can be easily added to a Grafana instance. ## Development ### Building ``` go get github.com/tynany/frr_exporter cd ${GOPATH}/src/github.com/prometheus/frr_exporter go build ``` ### Dev Environment A Docker-based development environment is available in the `dev/` directory for testing against real FRR instances. It includes BGP, OSPF, BFD, PIM, and VRRP configurations across multiple VRFs. ```bash cd dev make up build deploy make metrics1 ``` See [dev/README.md](dev/README.md) for full documentation, including macOS setup instructions. ### Linting This project uses https://golangci-lint.run in GitHub Actions. You can lint your code locally before submitting a PR by following the installation instructions at https://golangci-lint.run/usage/install/ and run prior to submitting changes: ``` golangci-lint run ``` ## TODO - Collector and main tests - OSPF6 - ISIS - Additional BGP SAFI - Feel free to submit a new feature request prometheus-frr-exporter-1.11.0/collector/000077500000000000000000000000001516444222400204145ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/collector/bfd.go000066400000000000000000000060511516444222400215000ustar00rootroot00000000000000package collector import ( "encoding/json" "log/slog" "github.com/prometheus/client_golang/prometheus" ) var bfdSubsystem = "bfd" func init() { registerCollector(bfdSubsystem, enabledByDefault, NewBFDCollector) } type bfdCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewBFDCollector collects BFD metrics, implemented as per the Collector interface. func NewBFDCollector(logger *slog.Logger) (Collector, error) { return &bfdCollector{logger: logger, descriptions: getBFDDesc()}, nil } func getBFDDesc() map[string]*prometheus.Desc { countLabels := []string{} peerLabels := []string{"local", "peer", "iface", "vrf"} return map[string]*prometheus.Desc{ "bfdPeerCount": colPromDesc(bfdSubsystem, "peer_count", "Number of peers detected.", countLabels), "bfdPeerUptime": colPromDesc(bfdSubsystem, "peer_uptime", "Uptime of bfd peer in seconds", peerLabels), "bfdPeerState": colPromDesc(bfdSubsystem, "peer_state", "State of the bfd peer (1 = Up, 0 = Down).", peerLabels), } } // Update implemented as per the Collector interface. func (c *bfdCollector) Update(ch chan<- prometheus.Metric) error { cmd := "show bfd peers json" jsonBFDInterface, err := executeBFDCommand(cmd) if err != nil { return err } if err = processBFDPeers(ch, jsonBFDInterface, c.descriptions); err != nil { return cmdOutputProcessError(cmd, string(jsonBFDInterface), err) } return nil } func processBFDPeers(ch chan<- prometheus.Metric, jsonBFDInterface []byte, bfdDesc map[string]*prometheus.Desc) error { var bfdPeers []bfdPeer if err := json.Unmarshal(jsonBFDInterface, &bfdPeers); err != nil { return err } // metric is a count of the number of peers newGauge(ch, bfdDesc["bfdPeerCount"], float64(len(bfdPeers))) for _, p := range bfdPeers { labels := []string{p.Local, p.Peer, p.Interface, p.Vrf} // get the uptime of the connection to the peer in seconds newGauge(ch, bfdDesc["bfdPeerUptime"], float64(p.Uptime), labels...) // state of connection to the bfd peer, up or down var bfdState float64 if p.Status == "up" { bfdState = 1 } newGauge(ch, bfdDesc["bfdPeerState"], bfdState, labels...) } return nil } type bfdPeer struct { Multihop bool `json:"multihop"` Peer string `json:"peer"` Local string `json:"local"` Interface string `json:"interface"` Vrf string `json:"vrf"` ID uint32 `json:"id"` RemoteID uint32 `json:"remote-id"` Status string `json:"status"` Uptime uint64 `json:"uptime"` Diagnostic string `json:"diagnostic"` RemoteDiagnostic string `json:"remote-diagnostic"` ReceiveInterval uint32 `json:"receive-interval"` TransmitInterval uint32 `json:"transmit-interval"` EchoInterval uint32 `json:"echo-interval"` RemoteReceiveInterval uint32 `json:"remote-receive-interval"` RemoteTransmitInterval uint32 `json:"remote-transmit-interval"` RemoteEchoInterval uint32 `json:"remote-echo-interval"` } prometheus-frr-exporter-1.11.0/collector/bfd_test.go000066400000000000000000000047561516444222400225510ustar00rootroot00000000000000package collector import ( "fmt" "regexp" "strings" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) var expectedBFDMetrics = map[string]float64{ "frr_bfd_peer_count{}": 3, "frr_bfd_peer_uptime{iface=eth0,local=10.10.141.81,peer=10.10.141.61,vrf=default}": 847716, "frr_bfd_peer_state{iface=eth0,local=10.10.141.81,peer=10.10.141.61,vrf=default}": 1, "frr_bfd_peer_uptime{iface=eth1,local=10.10.141.81,peer=10.10.141.62,vrf=blue}": 847595, "frr_bfd_peer_state{iface=eth1,local=10.10.141.81,peer=10.10.141.62,vrf=blue}": 1, "frr_bfd_peer_uptime{iface=,local=10.10.141.81,peer=10.10.141.63,vrf=default}": 847888, "frr_bfd_peer_state{iface=,local=10.10.141.81,peer=10.10.141.63,vrf=default}": 0, } func TestProcessBFDPeers(t *testing.T) { ch := make(chan prometheus.Metric, 1024) if err := processBFDPeers(ch, readTestFixture(t, "show_bfd_peers.json"), getBFDDesc()); err != nil { t.Errorf("error calling processBFDPeers ipv4unicast: %s", err) } close(ch) // Create a map of following format: // key: metric_name{labelname:labelvalue,...} // value: metric value gotMetrics := make(map[string]float64) for { msg, more := <-ch if !more { break } metric := &dto.Metric{} if err := msg.Write(metric); err != nil { t.Errorf("error writing metric: %s", err) } var labels []string for _, label := range metric.GetLabel() { labels = append(labels, fmt.Sprintf("%s=%s", label.GetName(), label.GetValue())) } var value float64 if metric.GetCounter() != nil { value = metric.GetCounter().GetValue() } else if metric.GetGauge() != nil { value = metric.GetGauge().GetValue() } re, err := regexp.Compile(`.*fqName: "(.*)", help:.*`) if err != nil { t.Errorf("could not compile regex: %s", err) } metricName := re.FindStringSubmatch(msg.Desc().String())[1] gotMetrics[fmt.Sprintf("%s{%s}", metricName, strings.Join(labels, ","))] = value } for metricName, metricVal := range gotMetrics { if expectedMetricVal, ok := expectedBFDMetrics[metricName]; ok { if expectedMetricVal != metricVal { t.Errorf("metric %s expected value %v got %v", metricName, expectedMetricVal, metricVal) } } else { t.Errorf("unexpected metric: %s : %v", metricName, metricVal) } } for expectedMetricName, expectedMetricVal := range expectedBFDMetrics { if _, ok := gotMetrics[expectedMetricName]; !ok { t.Errorf("missing metric: %s value %v", expectedMetricName, expectedMetricVal) } } } prometheus-frr-exporter-1.11.0/collector/bgp.go000066400000000000000000000631751516444222400215270ustar00rootroot00000000000000package collector import ( "bufio" "encoding/json" "fmt" "log/slog" "net/netip" "os" "strconv" "strings" "sync" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" ) var ( bgpSubsystem = "bgp" bgpPeerTypes = kingpin.Flag("collector.bgp.peer-types", "Enable the frr_bgp_peer_types_up metric (default: disabled).").Default("False").Bool() frrBGPDescKey = kingpin.Flag("collector.bgp.peer-types.keys", "Select the keys from the JSON formatted BGP peer description of which the values will be used with the frr_bgp_peer_types_up metric. Supports multiple values (default: type).").Default("type").Strings() bgpPeerDescs = kingpin.Flag("collector.bgp.peer-descriptions", "Add the value of the desc key from the JSON formatted BGP peer description as a label to peer metrics. (default: disabled).").Default("False").Bool() bgpPeerGroups = kingpin.Flag("collector.bgp.peer-groups", "Adds the peer's peer group name as a label. (default: disabled).").Default("False").Bool() bgpPeerHostnames = kingpin.Flag("collector.bgp.peer-hostnames", "Adds the peer's hostname as a label. (default: disabled).").Default("False").Bool() bgpPeerDescsText = kingpin.Flag("collector.bgp.peer-descriptions.plain-text", "Use the full text field of the BGP peer description instead of the value of the JSON formatted desc key (default: disabled).").Default("False").Bool() bgpAdvertisedPrefixes = kingpin.Flag("collector.bgp.advertised-prefixes", "Enables the frr_exporter_bgp_prefixes_advertised_count_total metric which exports the number of advertised prefixes to a BGP peer. This is an option for older versions of FRR that don't have PfxSent field (default: disabled).").Default("False").Bool() bgpAcceptedFilteredPrefixes = kingpin.Flag("collector.bgp.accepted-filtered-prefixes", "Enable retrieval of accepted and filtered BGP prefix counts (default: disabled).").Default("False").Bool() bgpNextHopInterface = kingpin.Flag("collector.bgp.next-hop-interface", "Adds the peer's next-hop interface label. (default: disabled).").Default("False").Bool() bgpMonitoredPrefixes = kingpin.Flag("collector.bgp.monitored-prefixes", "Path to a file listing prefixes to monitor for per-peer presence (one per line, # comments allowed).").Default("").String() ) func init() { registerCollector(bgpSubsystem, enabledByDefault, NewBGPCollector) registerCollector(bgpSubsystem+"6", disabledByDefault, NewBGP6Collector) registerCollector(bgpSubsystem+"l2vpn", disabledByDefault, NewBGPL2VPNCollector) } type bgpCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc afi string monitoredPrefixes []string } // NewBGPCollector collects BGP metrics, implemented as per the Collector interface. func NewBGPCollector(logger *slog.Logger) (Collector, error) { var prefixes []string if *bgpMonitoredPrefixes != "" { var err error prefixes, err = loadPrefixFilter(*bgpMonitoredPrefixes) if err != nil { return nil, err } } return &bgpCollector{logger: logger, descriptions: getBGPDesc(), afi: "ipv4", monitoredPrefixes: prefixes}, nil } func getBGPDesc() map[string]*prometheus.Desc { bgpLabels := []string{"vrf", "afi", "safi", "local_as"} bgpPeerTypeLabels := []string{"type", "afi", "safi"} bgpPeerLabels := append(bgpLabels, "peer", "peer_as") if *bgpPeerDescs { bgpPeerLabels = append(bgpPeerLabels, "peer_desc") } if *bgpPeerHostnames { bgpPeerLabels = append(bgpPeerLabels, "peer_hostname") } if *bgpPeerGroups { bgpPeerLabels = append(bgpPeerLabels, "peer_group") } if *bgpNextHopInterface { bgpPeerLabels = append(bgpPeerLabels, "nexthop_interface") } bgpPeerPrefixLabels := append(append([]string{}, bgpPeerLabels...), "prefix") return map[string]*prometheus.Desc{ "ribCount": colPromDesc(bgpSubsystem, "rib_count_total", "Number of routes in the RIB.", bgpLabels), "ribMemory": colPromDesc(bgpSubsystem, "rib_memory_bytes", "Memory consumbed by the RIB.", bgpLabels), "peerCount": colPromDesc(bgpSubsystem, "peers_count_total", "Number peers configured.", bgpLabels), "peerMemory": colPromDesc(bgpSubsystem, "peers_memory_bytes", "Memory consumed by peers.", bgpLabels), "peerGroupCount": colPromDesc(bgpSubsystem, "peer_groups_count_total", "Number of peer groups configured.", bgpLabels), "peerGroupMemory": colPromDesc(bgpSubsystem, "peer_groups_memory_bytes", "Memory consumed by peer groups.", bgpLabels), "msgRcvd": colPromDesc(bgpSubsystem, "peer_message_received_total", "Number of received messages.", bgpPeerLabels), "msgSent": colPromDesc(bgpSubsystem, "peer_message_sent_total", "Number of sent messages.", bgpPeerLabels), "prefixReceivedCount": colPromDesc(bgpSubsystem, "peer_prefixes_received_count_total", "Number of prefixes received.", bgpPeerLabels), "prefixAdvertisedCount": colPromDesc(bgpSubsystem, "peer_prefixes_advertised_count_total", "Number of prefixes advertised.", bgpPeerLabels), "prefixAcceptedCount": colPromDesc(bgpSubsystem, "peer_prefixes_accepted_count_total", "Number of prefixes accepted.", bgpPeerLabels), "prefixFilteredCount": colPromDesc(bgpSubsystem, "peer_prefixes_filtered_count_total", "Number of prefixes filtered.", bgpPeerLabels), "state": colPromDesc(bgpSubsystem, "peer_state", "State of the peer (2 = Administratively Down, 1 = Established, 0 = Down).", bgpPeerLabels), "UptimeSec": colPromDesc(bgpSubsystem, "peer_uptime_seconds", "How long has the peer been up.", bgpPeerLabels), "peerTypesUp": colPromDesc(bgpSubsystem, "peer_types_up", "Total Number of Peer Types that are Up.", bgpPeerTypeLabels), "prefixReceived": colPromDesc(bgpSubsystem, "peer_prefix_received", "Whether a monitored prefix is received from the peer (1 = present, 0 = absent).", bgpPeerPrefixLabels), "prefixAdvertised": colPromDesc(bgpSubsystem, "peer_prefix_advertised", "Whether a monitored prefix is advertised to the peer (1 = present, 0 = absent).", bgpPeerPrefixLabels), } } // Update implemented as per the Collector interface. func (c *bgpCollector) Update(ch chan<- prometheus.Metric) error { return collectBGP(ch, c.afi, c.logger, c.descriptions, c.monitoredPrefixes) } // NewBGP6Collector collects BGPv6 metrics, implemented as per the Collector interface. func NewBGP6Collector(logger *slog.Logger) (Collector, error) { var prefixes []string if *bgpMonitoredPrefixes != "" { var err error prefixes, err = loadPrefixFilter(*bgpMonitoredPrefixes) if err != nil { return nil, err } } return &bgpCollector{logger: logger, descriptions: getBGPDesc(), afi: "ipv6", monitoredPrefixes: prefixes}, nil } type bgpL2VPNCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewBGPL2VPNCollector collects BGP L2VPN metrics, implemented as per the Collector interface. func NewBGPL2VPNCollector(logger *slog.Logger) (Collector, error) { return &bgpL2VPNCollector{logger: logger, descriptions: getBGPL2VPNDesc()}, nil } func getBGPL2VPNDesc() map[string]*prometheus.Desc { bgpDesc := getBGPDesc() labels := []string{"vni", "type", "vxlanIf", "tenantVrf"} metricPrefix := "bgp_l2vpn_evpn" bgpDesc["numMacs"] = colPromDesc(metricPrefix, "mac_count_total", "Number of known MAC addresses", labels) bgpDesc["numArpNd"] = colPromDesc(metricPrefix, "arp_nd_count_total", "Number of ARP / ND entries", labels) bgpDesc["numRemoteVteps"] = colPromDesc(metricPrefix, "remote_vtep_count_total", "Number of known remote VTEPs. A value of -1 indicates a non-integer output from FRR, such as n/a.", labels) return bgpDesc } // Update implemented as per the Collector interface. func (c *bgpL2VPNCollector) Update(ch chan<- prometheus.Metric) error { if err := collectBGP(ch, "l2vpn", c.logger, c.descriptions, nil); err != nil { return err } cmd := "show evpn vni json" jsonBGPL2vpnEvpnSum, err := executeZebraCommand(cmd) if err != nil { return err } if len(jsonBGPL2vpnEvpnSum) == 0 { return nil } if err := processBgpL2vpnEvpnSummary(ch, jsonBGPL2vpnEvpnSum, c.descriptions); err != nil { return cmdOutputProcessError(cmd, string(jsonBGPL2vpnEvpnSum), err) } return nil } type vxLanStats struct { Vni uint32 VxlanType string `json:"type"` VxlanIf string NumMacs uint32 NumArpNd uint32 NumRemoteVteps interface{} // it's possible for the numRemoteVteps field to contain non-int values such as "n\/a" TenantVrf string } func processBgpL2vpnEvpnSummary(ch chan<- prometheus.Metric, jsonBGPL2vpnEvpnSum []byte, bgpL2vpnDesc map[string]*prometheus.Desc) error { var jsonMap map[string]vxLanStats if err := json.Unmarshal(jsonBGPL2vpnEvpnSum, &jsonMap); err != nil { return err } for _, vxLanStat := range jsonMap { bgpL2vpnLabels := []string{strconv.FormatUint(uint64(vxLanStat.Vni), 10), vxLanStat.VxlanType, vxLanStat.VxlanIf, vxLanStat.TenantVrf} newGauge(ch, bgpL2vpnDesc["numMacs"], float64(vxLanStat.NumMacs), bgpL2vpnLabels...) newGauge(ch, bgpL2vpnDesc["numArpNd"], float64(vxLanStat.NumArpNd), bgpL2vpnLabels...) remoteVteps, ok := vxLanStat.NumRemoteVteps.(float64) if !ok { remoteVteps = -1 } newGauge(ch, bgpL2vpnDesc["numRemoteVteps"], remoteVteps, bgpL2vpnLabels...) } return nil } func collectBGP(ch chan<- prometheus.Metric, AFI string, logger *slog.Logger, desc map[string]*prometheus.Desc, monitoredPrefixes []string) error { SAFI := "" switch AFI { case "ipv4", "ipv6": SAFI = "" case "l2vpn": SAFI = "evpn" } cmd := fmt.Sprintf("show bgp vrf all %s %s summary json", AFI, SAFI) jsonBGPSum, err := executeBGPCommand(cmd) if err != nil { return err } if err := processBGPSummary(ch, jsonBGPSum, AFI, SAFI, logger, desc, monitoredPrefixes); err != nil { return cmdOutputProcessError(cmd, string(jsonBGPSum), err) } return nil } func processBGPSummary(ch chan<- prometheus.Metric, jsonBGPSum []byte, AFI string, SAFI string, logger *slog.Logger, bgpDesc map[string]*prometheus.Desc, monitoredPrefixes []string) error { var jsonMap map[string]map[string]bgpProcess // if we've specified SAFI in the command, we won't have the SAFI layer of array to loop through // so we simulate it here, rather than using a conditional and writing almost the same code twice if AFI == "l2vpn" && SAFI == "evpn" { // since we need to massage the format a bit, unmarshall into a temp variable var tempJSONMap map[string]bgpProcess if err := json.Unmarshal(jsonBGPSum, &tempJSONMap); err != nil { return err } jsonMap = map[string]map[string]bgpProcess{} for vrfName, vrfData := range tempJSONMap { jsonMap[vrfName] = map[string]bgpProcess{"xxxxevpn": vrfData} } } else { // we have the format we expect, unmarshall directly into jsonMap if err := json.Unmarshal(jsonBGPSum, &jsonMap); err != nil { return err } } var peerDesc map[string]bgpVRF var err error if *bgpPeerTypes || *bgpPeerDescs || *bgpPeerGroups { peerDesc, err = getBGPPeerDesc() if err != nil { return err } } var bgpNextHop map[string]bgpNextHop if *bgpNextHopInterface { bgpNextHop, err = getBGPNexthop() if err != nil { return err } } peerTypes := make(map[string]map[string]float64) wg := &sync.WaitGroup{} for vrfName, vrfData := range jsonMap { for safiName, safiData := range vrfData { // The labels are "vrf", "afi", "safi", "local_as" localAs := strconv.FormatUint(uint64(safiData.AS), 10) procLabels := []string{strings.ToLower(vrfName), strings.ToLower(AFI), strings.ToLower(safiName[4:]), localAs} // No point collecting metrics if no peers configured. if safiData.PeerCount != 0 { newGauge(ch, bgpDesc["ribCount"], float64(safiData.RIBCount), procLabels...) newGauge(ch, bgpDesc["ribMemory"], float64(safiData.RIBMemory), procLabels...) newGauge(ch, bgpDesc["peerCount"], float64(safiData.PeerCount), procLabels...) newGauge(ch, bgpDesc["peerMemory"], float64(safiData.PeerMemory), procLabels...) newGauge(ch, bgpDesc["peerGroupCount"], float64(safiData.PeerGroupCount), procLabels...) newGauge(ch, bgpDesc["peerGroupMemory"], float64(safiData.PeerGroupMemory), procLabels...) for peerIP, peerData := range safiData.Peers { // The labels are "vrf", "afi", "safi", "local_as", "peer", "remote_as" peerLabels := []string{strings.ToLower(vrfName), strings.ToLower(AFI), strings.ToLower(safiName[4:]), localAs, peerIP, strconv.FormatUint(uint64(peerData.RemoteAs), 10)} if *bgpPeerDescs { d := peerDesc[vrfName].BGPNeighbors[peerIP].Desc if *bgpPeerDescsText { // The labels are "vrf", "afi", "safi", "local_as", "peer", "remote_as", "peer_desc" peerLabels = append(peerLabels, d) } else { // Assume the FRR BGP neighbor description is JSON formatted, and the description is in the "desc" field. jsonDesc := struct{ Desc string }{} if err := json.Unmarshal([]byte(d), &jsonDesc); err != nil { // Don't return an error as unmarshalling is best effort. logger.Error("cannot unmarshal bgp description", "description", peerDesc[vrfName].BGPNeighbors[peerIP].Desc, "err", err) } // The labels are "vrf", "afi", "safi", "local_as", "peer", "remote_as", "peer_desc" peerLabels = append(peerLabels, jsonDesc.Desc) } } if *bgpPeerHostnames { peerLabels = append(peerLabels, peerData.Hostname) } if *bgpPeerGroups { peerLabels = append(peerLabels, peerDesc[vrfName].BGPNeighbors[peerIP].PeerGroup) } if *bgpNextHopInterface { familyMap := map[string]map[string]bgpNextHopInterfaces{ "ipv4": bgpNextHop[vrfName].IPv4, "ipv6": bgpNextHop[vrfName].IPv6, } key := strings.ToLower(AFI) if nexthop, ok := familyMap[key][peerIP]; !ok { logger.Warn("BGP next hop not found", "afi", AFI, "peer", peerIP) peerLabels = append(peerLabels, "unknown") } else { if len(nexthop.Nexthops) > 0 { peerLabels = append(peerLabels, nexthop.Nexthops[0].InterfaceName) } else { peerLabels = append(peerLabels, "unknown") } } } // In earlier versions of FRR did not expose a summary of advertised prefixes for all peers, but in later versions it can get with PfxSnt field. if peerData.PfxSnt != nil { newGauge(ch, bgpDesc["prefixAdvertisedCount"], float64(*peerData.PfxSnt), peerLabels...) } else if *bgpAdvertisedPrefixes { wg.Add(1) go getPeerAdvertisedPrefixes(ch, wg, AFI, safiName[4:], vrfName, peerIP, logger, bgpDesc, peerLabels...) } newCounter(ch, bgpDesc["msgRcvd"], float64(peerData.MsgRcvd), peerLabels...) newCounter(ch, bgpDesc["msgSent"], float64(peerData.MsgSent), peerLabels...) newGauge(ch, bgpDesc["UptimeSec"], float64(peerData.PeerUptimeMsec)*0.001, peerLabels...) // In earlier versions of FRR, the prefixReceivedCount JSON element is used for the number of received prefixes, but in later versions it was changed to PfxRcd. prefixReceived := 0.0 if peerData.PrefixReceivedCount != 0 { prefixReceived = float64(peerData.PrefixReceivedCount) } else if peerData.PfxRcd != 0 { prefixReceived = float64(peerData.PfxRcd) } newGauge(ch, bgpDesc["prefixReceivedCount"], prefixReceived, peerLabels...) if *bgpAcceptedFilteredPrefixes { wg.Add(1) go getPeerAcceptedFilteredRoutes(ch, wg, AFI, safiName[4:], vrfName, peerIP, prefixReceived, logger, bgpDesc, peerLabels...) } var peerDescTypes map[string]string if *bgpPeerTypes { if err := json.Unmarshal([]byte(peerDesc[vrfName].BGPNeighbors[peerIP].Desc), &peerDescTypes); err != nil { // Don't return an error as unmarshalling is best effort. logger.Error("cannot unmarshal bgp description", "description", peerDesc[vrfName].BGPNeighbors[peerIP].Desc, "err", err) } // add key for this SAFI if it doesn't exist if _, exist := peerTypes[strings.ToLower(safiName[4:])]; !exist { peerTypes[strings.ToLower(safiName[4:])] = make(map[string]float64) } for _, descKey := range *frrBGPDescKey { if peerDescTypes[descKey] != "" { if _, exist := peerTypes[strings.ToLower(safiName[4:])][strings.TrimSpace(peerDescTypes[descKey])]; !exist { peerTypes[strings.ToLower(safiName[4:])][strings.TrimSpace(peerDescTypes[descKey])] = 0 } } } } peerState := 0.0 switch peerDataState := strings.ToLower(peerData.State); peerDataState { case "established": peerState = 1 if *bgpPeerTypes { for _, descKey := range *frrBGPDescKey { if peerDescTypes[descKey] != "" { peerTypes[strings.ToLower(safiName[4:])][strings.TrimSpace(peerDescTypes[descKey])]++ } } } if len(monitoredPrefixes) > 0 { wg.Add(1) go getPeerPrefixPresence(ch, wg, AFI, safiName[4:], vrfName, peerIP, monitoredPrefixes, logger, bgpDesc, peerLabels...) } case "idle (admin)": peerState = 2 } newGauge(ch, bgpDesc["state"], peerState, peerLabels...) } } } } wg.Wait() for peerSafi, peerTypesPerSafi := range peerTypes { for peerType, count := range peerTypesPerSafi { peerTypeLabels := []string{peerType, strings.ToLower(AFI), peerSafi} newGauge(ch, bgpDesc["peerTypesUp"], count, peerTypeLabels...) } } return nil } func getPeerAdvertisedPrefixes(ch chan<- prometheus.Metric, wg *sync.WaitGroup, AFI string, SAFI string, vrfName string, neighbor string, logger *slog.Logger, bgpDesc map[string]*prometheus.Desc, peerLabels ...string) { defer wg.Done() var cmd string if strings.ToLower(vrfName) == "default" { cmd = fmt.Sprintf("show bgp %s %s neighbors %s advertised-routes json", AFI, SAFI, neighbor) } else { cmd = fmt.Sprintf("show bgp vrf %s %s %s neighbors %s advertised-routes json", vrfName, AFI, SAFI, neighbor) } output, err := executeBGPCommand(cmd) if err != nil { logger.Error("get neighbor advertised prefixes failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } var advertisedPrefixes bgpAdvertisedRoutes if err := json.Unmarshal(output, &advertisedPrefixes); err != nil { logger.Error("get neighbor advertised prefixes failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } newGauge(ch, bgpDesc["prefixAdvertisedCount"], float64(advertisedPrefixes.TotalPrefixCounter), peerLabels...) } type bgpRoutes struct { // We care only about the routes Routes map[string][]json.RawMessage `json:"routes"` } func getPeerAcceptedFilteredRoutes(ch chan<- prometheus.Metric, wg *sync.WaitGroup, AFI string, SAFI string, vrfName string, neighbor string, prefixesReceived float64, logger *slog.Logger, bgpDesc map[string]*prometheus.Desc, peerLabels ...string) { defer wg.Done() var cmd string if strings.ToLower(vrfName) == "default" { cmd = fmt.Sprintf("show bgp %s %s neighbors %s routes json", strings.ToLower(AFI), strings.ToLower(SAFI), neighbor) } else { cmd = fmt.Sprintf("show bgp vrf %s %s %s neighbors %s routes json", vrfName, strings.ToLower(AFI), strings.ToLower(SAFI), neighbor) } output, err := executeBGPCommand(cmd) if err != nil { logger.Error("get neighbor accepted filtered routes failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } var routes bgpRoutes if err := json.Unmarshal(output, &routes); err != nil { logger.Error("get neighbor accepted filtered routes failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } prefixesAccepted := float64(len(routes.Routes)) newGauge(ch, bgpDesc["prefixAcceptedCount"], prefixesAccepted, peerLabels...) newGauge(ch, bgpDesc["prefixFilteredCount"], prefixesReceived-prefixesAccepted, peerLabels...) } func loadPrefixFilter(path string) ([]string, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var prefixes []string scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } if _, err := netip.ParsePrefix(line); err != nil { return nil, fmt.Errorf("invalid prefix %q: %w", line, err) } prefixes = append(prefixes, line) } return prefixes, scanner.Err() } type bgpAdvertisedRoutesDetailed struct { AdvertisedRoutes map[string]json.RawMessage `json:"advertisedRoutes"` } func processPeerPrefixPresence(ch chan<- prometheus.Metric, bgpDesc map[string]*prometheus.Desc, receivedPrefixes map[string]bool, advertisedPrefixes map[string]bool, monitoredPrefixes []string, peerLabels []string) { for _, prefix := range monitoredPrefixes { receivedVal := 0.0 if receivedPrefixes[prefix] { receivedVal = 1.0 } newGauge(ch, bgpDesc["prefixReceived"], receivedVal, append(peerLabels, prefix)...) advertisedVal := 0.0 if advertisedPrefixes[prefix] { advertisedVal = 1.0 } newGauge(ch, bgpDesc["prefixAdvertised"], advertisedVal, append(peerLabels, prefix)...) } } func getPeerPrefixPresence(ch chan<- prometheus.Metric, wg *sync.WaitGroup, AFI string, SAFI string, vrfName string, neighbor string, prefixes []string, logger *slog.Logger, bgpDesc map[string]*prometheus.Desc, peerLabels ...string) { defer wg.Done() var cmdReceived, cmdAdvertised string if strings.ToLower(vrfName) == "default" { cmdReceived = fmt.Sprintf("show bgp %s %s neighbors %s routes json", AFI, SAFI, neighbor) cmdAdvertised = fmt.Sprintf("show bgp %s %s neighbors %s advertised-routes json", AFI, SAFI, neighbor) } else { cmdReceived = fmt.Sprintf("show bgp vrf %s %s %s neighbors %s routes json", vrfName, AFI, SAFI, neighbor) cmdAdvertised = fmt.Sprintf("show bgp vrf %s %s %s neighbors %s advertised-routes json", vrfName, AFI, SAFI, neighbor) } receivedOutput, err := executeBGPCommand(cmdReceived) if err != nil { logger.Error("get neighbor received routes for prefix presence failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } var receivedRoutes bgpRoutes if err := json.Unmarshal(receivedOutput, &receivedRoutes); err != nil { logger.Error("get neighbor received routes for prefix presence failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } advertisedOutput, err := executeBGPCommand(cmdAdvertised) if err != nil { logger.Error("get neighbor advertised routes for prefix presence failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } var advertisedRoutes bgpAdvertisedRoutesDetailed if err := json.Unmarshal(advertisedOutput, &advertisedRoutes); err != nil { logger.Error("get neighbor advertised routes for prefix presence failed", "afi", AFI, "safi", SAFI, "vrf", vrfName, "neighbor", neighbor, "err", err) return } receivedSet := make(map[string]bool, len(receivedRoutes.Routes)) for k := range receivedRoutes.Routes { receivedSet[k] = true } advertisedSet := make(map[string]bool, len(advertisedRoutes.AdvertisedRoutes)) for k := range advertisedRoutes.AdvertisedRoutes { advertisedSet[k] = true } processPeerPrefixPresence(ch, bgpDesc, receivedSet, advertisedSet, prefixes, peerLabels) } type bgpProcess struct { RouterID string AS uint32 RIBCount uint32 RIBMemory uint32 PeerCount uint32 PeerMemory uint32 PeerGroupCount uint32 PeerGroupMemory uint32 Peers map[string]*bgpPeerSession } type bgpPeerSession struct { State string RemoteAs uint32 MsgRcvd uint32 MsgSent uint32 PeerUptimeMsec uint64 PrefixReceivedCount uint32 PfxRcd uint32 PfxSnt *uint32 Hostname string } type bgpAdvertisedRoutes struct { TotalPrefixCounter uint32 `json:"totalPrefixCounter"` } func getBGPPeerDesc() (map[string]bgpVRF, error) { output, err := executeBGPCommand("show bgp vrf all neighbors json") if err != nil { return nil, err } return processBGPPeerDesc(output) } func processBGPPeerDesc(output []byte) (map[string]bgpVRF, error) { vrfMap := make(map[string]bgpVRF) if err := json.Unmarshal([]byte(output), &vrfMap); err != nil { return nil, err } return vrfMap, nil } func (vrf *bgpVRF) UnmarshalJSON(data []byte) error { var raw map[string]*json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return err } vrf.BGPNeighbors = make(map[string]bgpNeighbor) for k, v := range raw { switch k { case "vrfId": if err := json.Unmarshal(*v, &vrf.ID); err != nil { return err } case "vrfName": // This is somewhat redundant, since the VRF name is a top-level key in the source JSON. if err := json.Unmarshal(*v, &vrf.Name); err != nil { return err } default: var neighbor bgpNeighbor if err := json.Unmarshal(*v, &neighbor); err != nil { return err } vrf.BGPNeighbors[k] = neighbor } } return nil } type bgpVRF struct { ID int `json:"vrfId"` Name string `json:"vrfName"` BGPNeighbors map[string]bgpNeighbor `json:"-"` } type bgpNeighbor struct { Desc string `json:"nbrDesc"` PeerGroup string `json:"peerGroup"` } type bgpNextHopInterfaces struct { Nexthops []struct { InterfaceName string `json:"interfaceName"` } `json:"nexthops"` } type bgpNextHop struct { IPv4 map[string]bgpNextHopInterfaces IPv6 map[string]bgpNextHopInterfaces } func getBGPNexthop() (map[string]bgpNextHop, error) { output, err := executeBGPCommand("show ip bgp vrf all nexthop json") if err != nil { return nil, err } return processBGPNexthop(output) } func processBGPNexthop(output []byte) (map[string]bgpNextHop, error) { bgpNextHop := make(map[string]bgpNextHop) if err := json.Unmarshal([]byte(output), &bgpNextHop); err != nil { return nil, err } return bgpNextHop, nil } prometheus-frr-exporter-1.11.0/collector/bgp_test.go000066400000000000000000000375171516444222400225670ustar00rootroot00000000000000package collector import ( "encoding/json" "log/slog" "path/filepath" "reflect" "testing" "github.com/prometheus/client_golang/prometheus" ) func runBGPSummaryTest(t *testing.T, fixture string, afi string, processFn func(chan<- prometheus.Metric, []byte, string, string, *slog.Logger, map[string]*prometheus.Desc, []string) error, getDesc func() map[string]*prometheus.Desc, expected map[string]float64) { // load the raw JSON data := readTestFixture(t, fixture) // enough buffer for instance=0 plus instances 1,2 ch := make(chan prometheus.Metric, len(expected)*3) if err := processFn(ch, data, afi, "", nil, getDesc(), nil); err != nil { t.Errorf("error calling processFn %s: %s", afi, err) } close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expected) } func TestProcessBGPSummary(t *testing.T) { expectedIpv4 := map[string]float64{ "frr_bgp_peer_groups_count_total{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_groups_count_total{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_groups_memory_bytes{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_groups_memory_bytes{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_message_received_total{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,safi=unicast,vrf=default}": 100.0, "frr_bgp_peer_message_received_total{afi=ipv4,local_as=64512,peer=192.168.0.3,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_message_received_total{afi=ipv4,local_as=64612,peer=192.168.1.2,peer_as=64613,safi=unicast,vrf=red}": 100.0, "frr_bgp_peer_message_received_total{afi=ipv4,local_as=64612,peer=192.168.1.3,peer_as=64614,safi=unicast,vrf=red}": 200.0, "frr_bgp_peer_message_sent_total{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,safi=unicast,vrf=default}": 100.0, "frr_bgp_peer_message_sent_total{afi=ipv4,local_as=64512,peer=192.168.0.3,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_message_sent_total{afi=ipv4,local_as=64612,peer=192.168.1.2,peer_as=64613,safi=unicast,vrf=red}": 100.0, "frr_bgp_peer_message_sent_total{afi=ipv4,local_as=64612,peer=192.168.1.3,peer_as=64614,safi=unicast,vrf=red}": 200.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv4,local_as=64512,peer=192.168.0.3,peer_as=64514,safi=unicast,vrf=default}": 2.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv4,local_as=64612,peer=192.168.1.2,peer_as=64613,safi=unicast,vrf=red}": 2.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv4,local_as=64612,peer=192.168.1.3,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peers_count_total{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 2.0, "frr_bgp_peers_count_total{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 2.0, "frr_bgp_peers_memory_bytes{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 39936.0, "frr_bgp_peers_memory_bytes{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 39936.0, "frr_bgp_peer_state{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,safi=unicast,vrf=default}": 1.0, "frr_bgp_peer_state{afi=ipv4,local_as=64512,peer=192.168.0.3,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_state{afi=ipv4,local_as=64612,peer=192.168.1.2,peer_as=64613,safi=unicast,vrf=red}": 1.0, "frr_bgp_peer_state{afi=ipv4,local_as=64612,peer=192.168.1.3,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_uptime_seconds{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,safi=unicast,vrf=default}": 10.0, "frr_bgp_peer_uptime_seconds{afi=ipv4,local_as=64512,peer=192.168.0.3,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_uptime_seconds{afi=ipv4,local_as=64612,peer=192.168.1.2,peer_as=64613,safi=unicast,vrf=red}": 20.0, "frr_bgp_peer_uptime_seconds{afi=ipv4,local_as=64612,peer=192.168.1.3,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_rib_count_total{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 1.0, "frr_bgp_rib_count_total{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_rib_memory_bytes{afi=ipv4,local_as=64512,safi=unicast,vrf=default}": 64.0, "frr_bgp_rib_memory_bytes{afi=ipv4,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_state{afi=ipv4,local_as=64512,peer=192.168.0.4,peer_as=64515,safi=unicast,vrf=default}": 2.0, "frr_bgp_peer_message_sent_total{afi=ipv4,local_as=64512,peer=192.168.0.4,peer_as=64515,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv4,local_as=64512,peer=192.168.0.4,peer_as=64515,safi=unicast,vrf=default}": 2.0, "frr_bgp_peer_uptime_seconds{afi=ipv4,local_as=64512,peer=192.168.0.4,peer_as=64515,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_message_received_total{afi=ipv4,local_as=64512,peer=192.168.0.4,peer_as=64515,safi=unicast,vrf=default}": 0.0, } runBGPSummaryTest(t, "show_bgp_vrf_all_ipv4_summary.json", "ipv4", processBGPSummary, getBGPDesc, expectedIpv4) expectedIpv6 := map[string]float64{ "frr_bgp_peers_memory_bytes{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 59904.0, "frr_bgp_peers_memory_bytes{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 59904.0, "frr_bgp_peers_count_total{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 2.0, "frr_bgp_peers_count_total{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 2.0, "frr_bgp_peer_groups_count_total{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_groups_count_total{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_groups_memory_bytes{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_groups_memory_bytes{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_message_received_total{afi=ipv6,local_as=64512,peer=fd00::1,peer_as=64513,safi=unicast,vrf=default}": 29285.0, "frr_bgp_peer_message_received_total{afi=ipv6,local_as=64512,peer=fd00::5,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_message_received_total{afi=ipv6,local_as=64612,peer=fd00::101,peer_as=64613,safi=unicast,vrf=red}": 29285.0, "frr_bgp_peer_message_received_total{afi=ipv6,local_as=64612,peer=fd00::105,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_message_sent_total{afi=ipv6,local_as=64512,peer=fd00::1,peer_as=64513,safi=unicast,vrf=default}": 29285.0, "frr_bgp_peer_message_sent_total{afi=ipv6,local_as=64512,peer=fd00::5,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_message_sent_total{afi=ipv6,local_as=64612,peer=fd00::101,peer_as=64613,safi=unicast,vrf=red}": 29285.0, "frr_bgp_peer_message_sent_total{afi=ipv6,local_as=64612,peer=fd00::105,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv6,local_as=64512,peer=fd00::1,peer_as=64513,safi=unicast,vrf=default}": 1.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv6,local_as=64512,peer=fd00::5,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv6,local_as=64612,peer=fd00::101,peer_as=64613,safi=unicast,vrf=red}": 1.0, "frr_bgp_peer_prefixes_received_count_total{afi=ipv6,local_as=64612,peer=fd00::105,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_state{afi=ipv6,local_as=64512,peer=fd00::1,peer_as=64513,safi=unicast,vrf=default}": 1.0, "frr_bgp_peer_state{afi=ipv6,local_as=64512,peer=fd00::5,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_state{afi=ipv6,local_as=64612,peer=fd00::101,peer_as=64613,safi=unicast,vrf=red}": 1.0, "frr_bgp_peer_state{afi=ipv6,local_as=64612,peer=fd00::105,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_peer_uptime_seconds{afi=ipv6,local_as=64512,peer=fd00::1,peer_as=64513,safi=unicast,vrf=default}": 8465643000.0, "frr_bgp_peer_uptime_seconds{afi=ipv6,local_as=64512,peer=fd00::5,peer_as=64514,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_uptime_seconds{afi=ipv6,local_as=64612,peer=fd00::101,peer_as=64613,safi=unicast,vrf=red}": 87873.0, "frr_bgp_peer_uptime_seconds{afi=ipv6,local_as=64612,peer=fd00::105,peer_as=64614,safi=unicast,vrf=red}": 0.0, "frr_bgp_rib_count_total{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 3.0, "frr_bgp_rib_count_total{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 3.0, "frr_bgp_rib_memory_bytes{afi=ipv6,local_as=64512,safi=unicast,vrf=default}": 456.0, "frr_bgp_rib_memory_bytes{afi=ipv6,local_as=64612,safi=unicast,vrf=red}": 456.0, } runBGPSummaryTest(t, "show_bgp_vrf_all_ipv6_summary.json", "ipv6", processBGPSummary, getBGPDesc, expectedIpv6) } func TestProcessBgpL2vpnEvpnSummary(t *testing.T) { expected := map[string]float64{ "frr_bgp_l2vpn_evpn_arp_nd_count_total{tenantVrf=default,type=L2,vni=172192,vxlanIf=ONTEP1_172192}": 23.000000, "frr_bgp_l2vpn_evpn_arp_nd_count_total{tenantVrf=default,type=L2,vni=174374,vxlanIf=ONTEP1_174374}": 0.000000, "frr_bgp_l2vpn_evpn_mac_count_total{tenantVrf=default,type=L2,vni=172192,vxlanIf=ONTEP1_172192}": 0.000000, "frr_bgp_l2vpn_evpn_mac_count_total{tenantVrf=default,type=L2,vni=174374,vxlanIf=ONTEP1_174374}": 42.000000, "frr_bgp_l2vpn_evpn_remote_vtep_count_total{tenantVrf=default,type=L2,vni=172192,vxlanIf=ONTEP1_172192}": -1.000000, "frr_bgp_l2vpn_evpn_remote_vtep_count_total{tenantVrf=default,type=L2,vni=174374,vxlanIf=ONTEP1_174374}": 1.000000, } ch := make(chan prometheus.Metric, 1024) if err := processBgpL2vpnEvpnSummary(ch, readTestFixture(t, "show_evpn_vni.json"), getBGPL2VPNDesc()); err != nil { t.Errorf("error calling processBgpL2vpnEvpnSummary: %s", err) } close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expected) } func TestProcessBGPPeerDesc(t *testing.T) { expectedOutput := map[string]bgpVRF{ "default": { ID: 0, Name: "default", BGPNeighbors: map[string]bgpNeighbor{ "10.1.1.10": {Desc: "{\"desc\":\"rt1\"}"}, "swp2": {Desc: "{\"desc\":\"fw1\"}"}, }, }, "vrf1": { ID: -1, Name: "vrf1", BGPNeighbors: map[string]bgpNeighbor{ "10.2.0.1": {Desc: "{\"desc\":\"remote\"}"}, }, }, } peerDesc, err := processBGPPeerDesc(readTestFixture(t, "show_bgp_vrf_all_neighbors.json")) if err != nil { t.Errorf("error calling processBGPPeerDesc: %s", err) } if !reflect.DeepEqual(peerDesc, expectedOutput) { t.Errorf("error comparing bgp neighbor description output: %v does not match expected %v", peerDesc, expectedOutput) } } func TestLoadPrefixFilter(t *testing.T) { got, err := loadPrefixFilter(filepath.Join("testdata", "prefix_filter.txt")) if err != nil { t.Fatalf("loadPrefixFilter returned error: %v", err) } expected := []string{"10.0.0.0/24", "10.0.1.0/24", "fd00::/64"} if !reflect.DeepEqual(got, expected) { t.Errorf("loadPrefixFilter() = %v, want %v", got, expected) } _, err = loadPrefixFilter(filepath.Join("testdata", "prefix_filter_invalid.txt")) if err == nil { t.Error("loadPrefixFilter should return error for invalid prefix") } } func TestProcessPeerPrefixPresence(t *testing.T) { receivedData := readTestFixture(t, "show_bgp_ipv4_unicast_neighbors_routes.json") advertisedData := readTestFixture(t, "show_bgp_ipv4_unicast_neighbors_advertised_routes.json") var receivedRoutes bgpRoutes if err := json.Unmarshal(receivedData, &receivedRoutes); err != nil { t.Fatalf("cannot unmarshal received routes: %v", err) } var advertisedRoutes bgpAdvertisedRoutesDetailed if err := json.Unmarshal(advertisedData, &advertisedRoutes); err != nil { t.Fatalf("cannot unmarshal advertised routes: %v", err) } receivedSet := make(map[string]bool, len(receivedRoutes.Routes)) for k := range receivedRoutes.Routes { receivedSet[k] = true } advertisedSet := make(map[string]bool, len(advertisedRoutes.AdvertisedRoutes)) for k := range advertisedRoutes.AdvertisedRoutes { advertisedSet[k] = true } prefixes := []string{"10.0.0.0/24", "10.0.1.0/24", "10.0.3.0/24", "10.0.99.0/24"} peerLabels := []string{"default", "ipv4", "unicast", "64512", "192.168.0.2", "64513"} desc := getBGPDesc() ch := make(chan prometheus.Metric, len(prefixes)*2) processPeerPrefixPresence(ch, desc, receivedSet, advertisedSet, prefixes, peerLabels) close(ch) gotMetrics := collectMetrics(t, ch) expected := map[string]float64{ // 10.0.0.0/24: received=yes, advertised=yes "frr_bgp_peer_prefix_received{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.0.0/24,safi=unicast,vrf=default}": 1.0, "frr_bgp_peer_prefix_advertised{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.0.0/24,safi=unicast,vrf=default}": 1.0, // 10.0.1.0/24: received=yes, advertised=no "frr_bgp_peer_prefix_received{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.1.0/24,safi=unicast,vrf=default}": 1.0, "frr_bgp_peer_prefix_advertised{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.1.0/24,safi=unicast,vrf=default}": 0.0, // 10.0.3.0/24: received=no, advertised=yes "frr_bgp_peer_prefix_received{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.3.0/24,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_prefix_advertised{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.3.0/24,safi=unicast,vrf=default}": 1.0, // 10.0.99.0/24: received=no, advertised=no "frr_bgp_peer_prefix_received{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.99.0/24,safi=unicast,vrf=default}": 0.0, "frr_bgp_peer_prefix_advertised{afi=ipv4,local_as=64512,peer=192.168.0.2,peer_as=64513,prefix=10.0.99.0/24,safi=unicast,vrf=default}": 0.0, } compareMetrics(t, gotMetrics, expected) } func TestProcessBGPNexthop(t *testing.T) { expected := map[string]bgpNextHop{ "default": { IPv4: map[string]bgpNextHopInterfaces{ "10.1.2.1": { Nexthops: []struct { InterfaceName string `json:"interfaceName"` }{ {InterfaceName: "eth1"}, }, }, "10.2.2.1": { Nexthops: []struct { InterfaceName string `json:"interfaceName"` }{ {InterfaceName: "eth2"}, }, }, }, IPv6: map[string]bgpNextHopInterfaces{}, }, } input := readTestFixture(t, "show_ip_bgp_vrf_all_nexthop.json") got, err := processBGPNexthop(input) if err != nil { t.Fatalf("processBGPNexthop returned error: %v", err) } if !reflect.DeepEqual(got, expected) { t.Errorf("processBGPNexthop() =\n%#v\nwant\n%#v", got, expected) } } prometheus-frr-exporter-1.11.0/collector/collector.go000066400000000000000000000133511516444222400227340ustar00rootroot00000000000000package collector import ( "fmt" "log/slog" "strconv" "strings" "sync" "time" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" "github.com/tynany/frr_exporter/internal/frrsockets" ) const ( metricNamespace = "frr" enabledByDefault = true disabledByDefault = false ) var ( socketConn *frrsockets.Connection frrTotalScrapeCount = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: metricNamespace, Name: "scrapes_total", Help: "Total number of times FRR has been scraped.", }) frrLabels = []string{"collector"} frrDesc = map[string]*prometheus.Desc{ "frrScrapeDuration": promDesc("scrape_duration_seconds", "Time it took for a collector's scrape to complete.", frrLabels), "frrCollectorUp": promDesc("collector_up", "Whether the collector's last scrape was successful (1 = successful, 0 = unsuccessful).", frrLabels), } socketDirPath = kingpin.Flag("frr.socket.dir-path", "Path of of the localstatedir containing each daemon's Unix socket.").Default("/var/run/frr").String() socketTimeout = kingpin.Flag("frr.socket.timeout", "Timeout when connecting to the FRR daemon Unix sockets").Default("20s").Duration() factories = make(map[string]func(logger *slog.Logger) (Collector, error)) initiatedCollectorsMtx = sync.Mutex{} initiatedCollectors = make(map[string]Collector) collectorState = make(map[string]*bool) ) func registerCollector(name string, enabledByDefaultStatus bool, factory func(logger *slog.Logger) (Collector, error)) { defaultState := "disabled" if enabledByDefaultStatus { defaultState = "enabled" } help := fmt.Sprintf("Enable the %s collector (default: %s).", name, defaultState) if enabledByDefaultStatus { help = fmt.Sprintf("Enable the %s collector (default: %s, to disable use --no-collector.%s).", name, defaultState, name) } factories[name] = factory collectorState[name] = kingpin.Flag(fmt.Sprintf("collector.%s", name), help).Default(strconv.FormatBool(enabledByDefaultStatus)).Bool() } // Collector is the interface a collector has to implement. type Collector interface { // Update metrics and sends to the Prometheus.Metric channel. Update(ch chan<- prometheus.Metric) error } // Exporter collects all collector metrics, implemented as per the prometheus.Collector interface. type Exporter struct { Collectors map[string]Collector logger *slog.Logger } // NewExporter returns a new Exporter. func NewExporter(logger *slog.Logger) (*Exporter, error) { collectors := make(map[string]Collector) initiatedCollectorsMtx.Lock() defer initiatedCollectorsMtx.Unlock() socketConn = frrsockets.NewConnection(*socketDirPath, *socketTimeout) for name, enabled := range collectorState { if !*enabled { continue } if collector, exists := initiatedCollectors[name]; exists { collectors[name] = collector } else { collector, err := factories[name](logger.With("collector", name)) if err != nil { return nil, err } collectors[name] = collector initiatedCollectors[name] = collector } } return &Exporter{ Collectors: collectors, logger: logger, }, nil } // Collect implemented as per the prometheus.Collector interface. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { frrTotalScrapeCount.Inc() ch <- frrTotalScrapeCount wg := &sync.WaitGroup{} wg.Add(len(e.Collectors)) for name, collector := range e.Collectors { go runCollector(ch, name, collector, wg, e.logger) } wg.Wait() } func runCollector(ch chan<- prometheus.Metric, name string, collector Collector, wg *sync.WaitGroup, logger *slog.Logger) { defer wg.Done() startTime := time.Now() err := collector.Update(ch) scrapeDurationSeconds := time.Since(startTime).Seconds() ch <- prometheus.MustNewConstMetric(frrDesc["frrScrapeDuration"], prometheus.GaugeValue, float64(scrapeDurationSeconds), name) success := 0.0 if err != nil { logger.Error("collector scrape failed", "name", name, "duration_seconds", scrapeDurationSeconds, "err", err) } else { logger.Debug("collector succeeded", "name", name, "duration_seconds", scrapeDurationSeconds) success = 1 } ch <- prometheus.MustNewConstMetric(frrDesc["frrCollectorUp"], prometheus.GaugeValue, success, name) } // Describe implemented as per the prometheus.Collector interface. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { for _, desc := range frrDesc { ch <- desc } } func promDesc(metricName string, metricDescription string, labels []string) *prometheus.Desc { return prometheus.NewDesc(metricNamespace+"_"+metricName, metricDescription, labels, nil) } func colPromDesc(subsystem string, metricName string, metricDescription string, labels []string) *prometheus.Desc { return prometheus.NewDesc(prometheus.BuildFQName(metricNamespace, subsystem, metricName), metricDescription, labels, nil) } func newGauge(ch chan<- prometheus.Metric, descName *prometheus.Desc, metric float64, labels ...string) { ch <- prometheus.MustNewConstMetric(descName, prometheus.GaugeValue, metric, labels...) } func newCounter(ch chan<- prometheus.Metric, descName *prometheus.Desc, metric float64, labels ...string) { ch <- prometheus.MustNewConstMetric(descName, prometheus.CounterValue, metric, labels...) } func cmdOutputProcessError(cmd, output string, err error) error { return fmt.Errorf("cannot process output of %s: %w: command output: %s", cmd, err, output) } func getVRFs() ([]string, error) { output, err := executeZebraCommand("show vrf") if err != nil { return nil, err } return parseVRFs(output), nil } func parseVRFs(output []byte) []string { vrfs := []string{"default"} for _, line := range strings.Split(string(output), "\n") { fields := strings.Fields(line) if len(fields) >= 2 && fields[0] == "vrf" { vrfs = append(vrfs, fields[1]) } } return vrfs } prometheus-frr-exporter-1.11.0/collector/collector_test.go000066400000000000000000000037201516444222400237720ustar00rootroot00000000000000package collector import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) func readTestFixture(t *testing.T, filename string) []byte { data, err := os.ReadFile(filepath.Join("testdata", filename)) if err != nil { t.Fatalf("cannot read test fixture: %v", err) } return data } func compareMetrics(t *testing.T, gotMetrics map[string]float64, expectedMetrics map[string]float64) { for metricName, metricVal := range gotMetrics { if expectedMetricVal, ok := expectedMetrics[metricName]; ok { if expectedMetricVal != metricVal { t.Errorf("metric %s expected value %v got %v", metricName, expectedMetricVal, metricVal) } } else { t.Errorf("unexpected metric: %s : %v", metricName, metricVal) } } for expectedMetricName, expectedMetricVal := range expectedMetrics { if _, ok := gotMetrics[expectedMetricName]; !ok { t.Errorf("missing metric: %s value %v", expectedMetricName, expectedMetricVal) } } } func collectMetrics(t *testing.T, ch <-chan prometheus.Metric) map[string]float64 { got := make(map[string]float64) re := regexp.MustCompile(`.*fqName: "(.*)", help:.*`) for m := range ch { var dtoM dto.Metric if err := m.Write(&dtoM); err != nil { t.Errorf("Write(): %v", err) continue } // build label strings WITHOUT quotes var lbls []string for _, l := range dtoM.GetLabel() { lbls = append(lbls, fmt.Sprintf("%s=%s", l.GetName(), l.GetValue())) } // sort them so the order is deterministic: area,iface,instance,vrf sort.Strings(lbls) // grab the numeric value var v float64 if c := dtoM.GetCounter(); c != nil { v = c.GetValue() } else if g := dtoM.GetGauge(); g != nil { v = g.GetValue() } // extract the metric name from the Desc() text name := re.FindStringSubmatch(m.Desc().String())[1] key := fmt.Sprintf("%s{%s}", name, strings.Join(lbls, ",")) got[key] = v } return got } prometheus-frr-exporter-1.11.0/collector/command.go000066400000000000000000000051141516444222400223620ustar00rootroot00000000000000package collector import ( "bytes" "context" "fmt" "os/exec" "strings" "github.com/alecthomas/kingpin/v2" ) var ( vtyshEnable = kingpin.Flag("frr.vtysh", "Use vtysh to query FRR instead of each daemon's Unix socket (default: disabled, recommended: disabled).").Default("false").Bool() vtyshPath = kingpin.Flag("frr.vtysh.path", "Path of vtysh.").Default("/usr/bin/vtysh").String() vtyshTimeout = kingpin.Flag("frr.vtysh.timeout", "The timeout when running vtysh commands (default: 20s).").Default("20s").Duration() vtyshSudo = kingpin.Flag("frr.vtysh.sudo", "Enable sudo when executing vtysh commands.").Bool() frrVTYSHOptions = kingpin.Flag("frr.vtysh.options", "Additional options passed to vtysh.").Default("").String() ) func executeBFDCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecBFDCmd(cmd) } func executeBGPCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecBGPCmd(cmd) } func executeOSPFMultiInstanceCommand(cmd string, instanceID int) ([]byte, error) { return socketConn.ExecOSPFMultiInstanceCmd(cmd, instanceID) } func executeOSPFCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecOSPFCmd(cmd) } func executePIMCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecPIMCmd(cmd) } func executeZebraCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecZebraCmd(cmd) } func executeVRRPCommand(cmd string) ([]byte, error) { if *vtyshEnable { return execVtyshCommand(cmd) } return socketConn.ExecVRRPCmd(cmd) } func execVtyshCommand(vtyshCmd string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), *vtyshTimeout) defer cancel() var a []string var executable string if *vtyshSudo { a = []string{*vtyshPath} executable = "/usr/bin/sudo" } else { a = []string{} executable = *vtyshPath } if *frrVTYSHOptions != "" { frrOptions := strings.Split(*frrVTYSHOptions, " ") a = append(a, frrOptions...) } a = append(a, "-c", vtyshCmd) cmd := exec.CommandContext(ctx, executable, a...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { return stdout.Bytes(), fmt.Errorf("command %s failed: %w: stderr: %s: stdout: %s", cmd, err, strings.ReplaceAll(stderr.String(), "\n", " "), strings.ReplaceAll(stdout.String(), "\n", " ")) } return stdout.Bytes(), nil } prometheus-frr-exporter-1.11.0/collector/ospf.go000066400000000000000000000341341516444222400217170ustar00rootroot00000000000000package collector import ( "encoding/json" "fmt" "log/slog" "strconv" "strings" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" ) var ( ospfSubsystem = "ospf" frrOSPFInstances = kingpin.Flag("collector.ospf.instances", "Comma-separated list of instance IDs if using multiple OSPF instances").Default("").String() ) func init() { registerCollector(ospfSubsystem, enabledByDefault, NewOSPFCollector) } type ospfCollector struct { logger *slog.Logger ospfIfaceDescriptions map[string]*prometheus.Desc ospfDescriptions map[string]*prometheus.Desc ospfNeighDescriptions map[string]*prometheus.Desc ospfDataMaxAgeDescriptions map[string]*prometheus.Desc instanceIDs []int } // NewOSPFCollector collects OSPF metrics, implemented as per the Collector interface. func NewOSPFCollector(logger *slog.Logger) (Collector, error) { var instanceIDs []int if len(*frrOSPFInstances) > 0 { // FRR Exporter does not support multi-instance when using `vtysh` to interface with FRR // via the `--frr.vtysh` flag for the following reasons: // * Invalid JSON is returned when OSPF commands are executed by `vtysh`. For example, // `show ip ospf vrf all interface json` returns the concatenated JSON from each OSPF instance. // * Vtysh does not support `vrf` and `instance` in the same commend. For example, // `show ip ospf 1 vrf all interface json` is an invalid command. if *vtyshEnable { return nil, fmt.Errorf("cannot use --frr.vtysh with --collector.ospf.instances") } instances := strings.Split(*frrOSPFInstances, ",") for _, id := range instances { i, err := strconv.Atoi(id) if err != nil { return nil, fmt.Errorf("unable to parse instance ID %s: %w", id, err) } instanceIDs = append(instanceIDs, i) } } return &ospfCollector{logger: logger, instanceIDs: instanceIDs, ospfIfaceDescriptions: getOSPFIfaceDesc(), ospfDescriptions: getOSPFDesc(), ospfNeighDescriptions: getOSPFNeighDesc(), ospfDataMaxAgeDescriptions: getOSPFDataMaxAgeDesc()}, nil } // Update satisfies Collector. func (c *ospfCollector) Update(ch chan<- prometheus.Metric) error { steps := []struct { cmd string desc map[string]*prometheus.Desc processor func(chan<- prometheus.Metric, []byte, map[string]*prometheus.Desc, int) error }{ { cmd: "show ip ospf vrf all json", desc: c.ospfDescriptions, processor: processOSPF, }, { cmd: "show ip ospf vrf all interface json", desc: c.ospfIfaceDescriptions, processor: processOSPFInterface, }, { cmd: "show ip ospf vrf all neighbor json", desc: c.ospfNeighDescriptions, processor: processOSPFNeigh, }, { cmd: "show ip ospf vrf all database max-age json", desc: c.ospfDataMaxAgeDescriptions, processor: processOSPFDataMaxAge, }, } for _, s := range steps { if err := c.update(ch, s.cmd, s.desc, s.processor); err != nil { return err } } return nil } func (c *ospfCollector) update( ch chan<- prometheus.Metric, cmd string, descriptions map[string]*prometheus.Desc, process func(chan<- prometheus.Metric, []byte, map[string]*prometheus.Desc, int) error, ) error { if len(c.instanceIDs) > 0 { for _, id := range c.instanceIDs { jsonBytes, err := executeOSPFMultiInstanceCommand(cmd, id) if err != nil { return err } if err := process(ch, jsonBytes, descriptions, id); err != nil { return cmdOutputProcessError(cmd, string(jsonBytes), err) } } return nil } jsonBytes, err := executeOSPFCommand(cmd) if err != nil { return err } if err := process(ch, jsonBytes, descriptions, 0); err != nil { return cmdOutputProcessError(cmd, string(jsonBytes), err) } return nil } func getOSPFIfaceDesc() map[string]*prometheus.Desc { labels := []string{"vrf", "iface", "area"} if len(*frrOSPFInstances) > 0 { labels = append(labels, "instance") } return map[string]*prometheus.Desc{ "ospfIfaceNeigh": colPromDesc(ospfSubsystem, "neighbors", "Number of neighbors detected.", labels), "ospfIfaceNeighAdj": colPromDesc(ospfSubsystem, "neighbor_adjacencies", "Number of neighbor adjacencies formed.", labels), } } func getOSPFDesc() map[string]*prometheus.Desc { routerLabels := []string{"vrf"} areaLabels := []string{"vrf", "area"} if len(*frrOSPFInstances) > 0 { routerLabels = append(routerLabels, "instance") areaLabels = append(areaLabels, "instance") } return map[string]*prometheus.Desc{ "ospfLsaExternalCounter": colPromDesc(ospfSubsystem, "lsa_external_counter", "Number of external LSAs.", routerLabels), "ospfLsaAsOpaqueCounter": colPromDesc(ospfSubsystem, "lsa_as_opaque_counter", "Number of AS Opaque LSAs.", routerLabels), "ospfAreaLsaNumber": colPromDesc(ospfSubsystem, "area_lsa_number", "Number of LSAs in the area.", areaLabels), "ospfAreaLsaNetworkNumber": colPromDesc(ospfSubsystem, "area_lsa_network_number", "Number of network LSAs in the area.", areaLabels), "ospfAreaLsaSummaryNumber": colPromDesc(ospfSubsystem, "area_lsa_summary_number", "Number of summary LSAs in the area.", areaLabels), "ospfAreaLsaAsbrNumber": colPromDesc(ospfSubsystem, "area_lsa_asbr_number", "Number of ASBR LSAs in the area.", areaLabels), "ospfAreaLsaNssaNumber": colPromDesc(ospfSubsystem, "area_lsa_nssa_number", "Number of NSSA LSAs in the area.", areaLabels), } } func getOSPFNeighDesc() map[string]*prometheus.Desc { var labels []string if len(*frrOSPFInstances) > 0 { labels = append(labels, "instance") } labels = append(labels, "vrf", "neighbor", "iface", "local_address", "remote_address") return map[string]*prometheus.Desc{ "ospfNeighState": colPromDesc(ospfSubsystem, "neighbor_state", "OSPF neighbor state (1=Down, 2=Init, 3=2-Way, 4=ExStart, 5=Exchange, 6=Loading, 7=Full).", labels), } } func getOSPFDataMaxAgeDesc() map[string]*prometheus.Desc { var labels []string if len(*frrOSPFInstances) > 0 { labels = append(labels, "instance") } labels = append(labels, "vrf") return map[string]*prometheus.Desc{ "ospfDataMaxAge": colPromDesc(ospfSubsystem, "data_ls_max_age", "Amount of link state max age entries.", labels), } } func processOSPFInterface(ch chan<- prometheus.Metric, jsonOSPFInterface []byte, ospfDesc map[string]*prometheus.Desc, instanceID int) error { // Unfortunately, the 'show ip ospf vrf all interface json' JSON output is poorly structured. Instead // of all interfaces being in a list, each interface is added as a key on the same level of vrfName and // vrfId. As such, we have to loop through each key and apply logic to determine whether the key is an // interface. var jsonMap map[string]json.RawMessage if err := json.Unmarshal(jsonOSPFInterface, &jsonMap); err != nil { return fmt.Errorf("cannot unmarshal ospf interface json: %s", err) } for vrfName, vrfData := range jsonMap { var _tempvrfInstance map[string]json.RawMessage switch vrfName { case "ospfInstance": // Do nothing default: if err := json.Unmarshal(vrfData, &_tempvrfInstance); err != nil { return fmt.Errorf("cannot unmarshal VRF instance json: %s", err) } } for ospfInstanceKey, ospfInstanceVal := range _tempvrfInstance { switch ospfInstanceKey { case "vrfName", "vrfId": // Do nothing as we do not need the value of these keys. case "interfaces": var _tempInterfaceInstance map[string]json.RawMessage if err := json.Unmarshal(ospfInstanceVal, &_tempInterfaceInstance); err != nil { return fmt.Errorf("cannot unmarshal VRF instance json: %s", err) } for interfaceKey, interfaceValue := range _tempInterfaceInstance { var newIface ospfIface if err := json.Unmarshal(interfaceValue, &newIface); err != nil { return fmt.Errorf("cannot unmarshal interface json: %s", err) } if !newIface.TimerPassiveIface { // The labels are "vrf", "newIface", "area" labels := []string{strings.ToLower(vrfName), interfaceKey, newIface.Area} ospfIfaceMetrics(ch, newIface, labels, ospfDesc, instanceID) } } default: // All other keys are interfaces. var iface ospfIface if err := json.Unmarshal(ospfInstanceVal, &iface); err != nil { return fmt.Errorf("cannot unmarshal interface json: %s", err) } if !iface.TimerPassiveIface { // The labels are "vrf", "iface", "area" labels := []string{strings.ToLower(vrfName), ospfInstanceKey, iface.Area} ospfIfaceMetrics(ch, iface, labels, ospfDesc, instanceID) } } } } return nil } func ospfIfaceMetrics(ch chan<- prometheus.Metric, iface ospfIface, labels []string, ospfDesc map[string]*prometheus.Desc, instanceID int) { if instanceID != 0 { labels = append(labels, strconv.Itoa(instanceID)) } newGauge(ch, ospfDesc["ospfIfaceNeigh"], float64(iface.NbrCount), labels...) newGauge(ch, ospfDesc["ospfIfaceNeighAdj"], float64(iface.NbrAdjacentCount), labels...) } type ospfIface struct { NbrCount uint32 NbrAdjacentCount uint32 Area string TimerPassiveIface bool } func processOSPF(ch chan<- prometheus.Metric, jsonOSPF []byte, ospfDesc map[string]*prometheus.Desc, instanceID int) error { var all map[string]ospfInstance if err := json.Unmarshal(jsonOSPF, &all); err != nil { return fmt.Errorf("cannot unmarshal ospf json: %w", err) } for vrfName, vrfData := range all { ospfMetrics(ch, vrfData, vrfName, ospfDesc, instanceID) } return nil } func ospfMetrics(ch chan<- prometheus.Metric, ospfData ospfInstance, vrfName string, ospfDesc map[string]*prometheus.Desc, instanceID int) { routerLabels := []string{strings.ToLower(vrfName)} if instanceID != 0 { routerLabels = append(routerLabels, strconv.Itoa(instanceID)) } newGauge(ch, ospfDesc["ospfLsaExternalCounter"], float64(ospfData.LsaExternalCounter), routerLabels...) newGauge(ch, ospfDesc["ospfLsaAsOpaqueCounter"], float64(ospfData.LsaAsopaqueCounter), routerLabels...) for areaName, area := range ospfData.Areas { areaLabels := []string{strings.ToLower(vrfName), areaName} if instanceID != 0 { areaLabels = append(areaLabels, strconv.Itoa(instanceID)) } newGauge(ch, ospfDesc["ospfAreaLsaNumber"], float64(area.LsaNumber), areaLabels...) newGauge(ch, ospfDesc["ospfAreaLsaNetworkNumber"], float64(area.LsaNetworkNumber), areaLabels...) newGauge(ch, ospfDesc["ospfAreaLsaSummaryNumber"], float64(area.LsaSummaryNumber), areaLabels...) newGauge(ch, ospfDesc["ospfAreaLsaAsbrNumber"], float64(area.LsaAsbrNumber), areaLabels...) newGauge(ch, ospfDesc["ospfAreaLsaNssaNumber"], float64(area.LsaNssaNumber), areaLabels...) } } type ospfInstance struct { LsaExternalCounter uint32 LsaAsopaqueCounter uint32 Areas map[string]ospfArea } type ospfArea struct { LsaNumber uint32 LsaNetworkNumber uint32 LsaSummaryNumber uint32 LsaAsbrNumber uint32 LsaNssaNumber uint32 } func processOSPFNeigh(ch chan<- prometheus.Metric, jsonOSPFNeigh []byte, ospfDesc map[string]*prometheus.Desc, instanceID int) error { var vrfNeighs map[string]vrfNeighbors if err := json.Unmarshal(jsonOSPFNeigh, &vrfNeighs); err != nil { return fmt.Errorf("cannot unmarshal ospf neighbor json: %w", err) } for vrfName, vrfData := range vrfNeighs { for neighborName, neighbors := range vrfData.Neighbors { ospfNeighMetrics(ch, neighborName, neighbors, vrfName, ospfDesc, instanceID) } } return nil } func ospfNeighMetrics(ch chan<- prometheus.Metric, neighborName string, neighbors []ospfNeighbor, vrfName string, ospfDesc map[string]*prometheus.Desc, instanceID int) { var labels []string if instanceID != 0 { labels = append(labels, strconv.Itoa(instanceID)) } labels = append(labels, strings.ToLower(vrfName), neighborName) for _, neighbor := range neighbors { var state float64 switch neighbor.State { case "Down": state = 1 case "Init": state = 2 case "2-Way": state = 3 case "ExStart": state = 4 case "Exchange": state = 5 case "Loading": state = 6 case "Full": state = 7 default: continue } newGauge(ch, ospfDesc["ospfNeighState"], state, append(labels, neighbor.IfaceName, neighbor.LocalAddress, neighbor.RemoteAddress)...) } } type vrfNeighbors struct { VRFName string Neighbors map[string][]ospfNeighbor } func GetOSPFState(nbrState, state string) string { if nbrState != "" { return nbrState } return state } type ospfNeighbor struct { State string `json:"state"` NbrState string `json:"nbrState"` IfaceName string `json:"ifaceName"` LocalAddress string `json:"localAddress"` RemoteAddress string `json:"address"` } func (n *ospfNeighbor) UnmarshalJSON(data []byte) error { var temp struct { NbrState string `json:"nbrState"` State string `json:"state"` IfaceName string `json:"ifaceName"` LocalAddr string `json:"localAddress"` RemoteAddr string `json:"address"` } if err := json.Unmarshal(data, &temp); err != nil { return fmt.Errorf("cannot unmarshal ospf neighbor json: %w", err) } iface := strings.Split(temp.IfaceName, ":") if len(iface) == 2 { n.IfaceName = iface[0] n.LocalAddress = iface[1] } else { return fmt.Errorf("cannot unmarshal ospf neighbor iface: %s", iface) } state := strings.Split(GetOSPFState(temp.NbrState, temp.State), "/") if len(state) > 0 { n.State = state[0] } else { return fmt.Errorf("cannot unmarshal ospf neighbor state: %s", state) } n.RemoteAddress = temp.RemoteAddr return nil } func processOSPFDataMaxAge(ch chan<- prometheus.Metric, jsonOSPFMaxAge []byte, ospfDesc map[string]*prometheus.Desc, instanceID int) error { var all map[string]ospfDataMaxAge if err := json.Unmarshal(jsonOSPFMaxAge, &all); err != nil { return fmt.Errorf("cannot unmarshal ospf max age json: %w", err) } for vrfName, vrfData := range all { ospfDataMaxAgeMetrics(ch, vrfData, vrfName, ospfDesc, instanceID) } return nil } func ospfDataMaxAgeMetrics(ch chan<- prometheus.Metric, ospfData ospfDataMaxAge, vrfName string, ospfDesc map[string]*prometheus.Desc, instanceID int) { labels := []string{strings.ToLower(vrfName)} if instanceID != 0 { labels = append(labels, strconv.Itoa(instanceID)) } newGauge(ch, ospfDesc["ospfDataMaxAge"], float64(len(ospfData.MaxAgeLinkStates)), labels...) } type ospfDataMaxAge struct { VRFName string MaxAgeLinkStates map[string]struct{} } prometheus-frr-exporter-1.11.0/collector/ospf_test.go000066400000000000000000000163341516444222400227600ustar00rootroot00000000000000package collector import ( "testing" "github.com/prometheus/client_golang/prometheus" ) // runOSPFTest is a one-stop helper. // - fixture: filename under testdata/ // - processFn: e.g. processOSPFInterface // - getDesc: e.g. getOSPFIfaceDesc // - expected: map[string]float64 func runOSPFTest( t *testing.T, fixture string, processFn func(chan<- prometheus.Metric, []byte, map[string]*prometheus.Desc, int) error, getDesc func() map[string]*prometheus.Desc, expected map[string]float64, ) { // load the raw JSON data := readTestFixture(t, fixture) // enough buffer for instance=0 plus instances 1,2 ch := make(chan prometheus.Metric, len(expected)*3) *frrOSPFInstances = "" if err := processFn(ch, data, getDesc(), 0); err != nil { t.Errorf("instance=0: %v", err) } *frrOSPFInstances = "1,2" for i := 1; i <= 2; i++ { if err := processFn(ch, data, getDesc(), i); err != nil { t.Errorf("instance=%d: %v", i, err) } } close(ch) got := collectMetrics(t, ch) compareMetrics(t, got, expected) } func TestProcessOSPFInterface(t *testing.T) { expected := map[string]float64{ "frr_ospf_neighbors{area=0.0.0.0,iface=swp1,vrf=default}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp2,vrf=default}": 1, "frr_ospf_neighbors{area=0.0.0.0,iface=swp3,vrf=red}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp4,vrf=red}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp1,vrf=default}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp2,vrf=default}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp3,vrf=red}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp4,vrf=red}": 1, "frr_ospf_neighbors{area=0.0.0.0,iface=swp1,instance=1,vrf=default}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp2,instance=1,vrf=default}": 1, "frr_ospf_neighbors{area=0.0.0.0,iface=swp3,instance=1,vrf=red}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp4,instance=1,vrf=red}": 1, "frr_ospf_neighbors{area=0.0.0.0,iface=swp1,instance=2,vrf=default}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp2,instance=2,vrf=default}": 1, "frr_ospf_neighbors{area=0.0.0.0,iface=swp3,instance=2,vrf=red}": 0, "frr_ospf_neighbors{area=0.0.0.0,iface=swp4,instance=2,vrf=red}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp1,instance=1,vrf=default}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp2,instance=1,vrf=default}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp3,instance=1,vrf=red}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp4,instance=1,vrf=red}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp1,instance=2,vrf=default}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp2,instance=2,vrf=default}": 1, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp3,instance=2,vrf=red}": 0, "frr_ospf_neighbor_adjacencies{area=0.0.0.0,iface=swp4,instance=2,vrf=red}": 1, } runOSPFTest( t, "show_ip_ospf_vrf_all_interface.json", processOSPFInterface, getOSPFIfaceDesc, expected, ) } func TestProcessOSPF(t *testing.T) { expected := map[string]float64{ "frr_ospf_lsa_external_counter{vrf=default}": 109, "frr_ospf_lsa_as_opaque_counter{vrf=default}": 0, "frr_ospf_area_lsa_number{area=0.0.0.0,vrf=default}": 17, "frr_ospf_area_lsa_network_number{area=0.0.0.0,vrf=default}": 1, "frr_ospf_area_lsa_summary_number{area=0.0.0.0,vrf=default}": 0, "frr_ospf_area_lsa_asbr_number{area=0.0.0.0,vrf=default}": 0, "frr_ospf_area_lsa_nssa_number{area=0.0.0.0,vrf=default}": 0, "frr_ospf_lsa_external_counter{instance=1,vrf=default}": 109, "frr_ospf_lsa_as_opaque_counter{instance=1,vrf=default}": 0, "frr_ospf_area_lsa_number{area=0.0.0.0,instance=1,vrf=default}": 17, "frr_ospf_area_lsa_network_number{area=0.0.0.0,instance=1,vrf=default}": 1, "frr_ospf_area_lsa_summary_number{area=0.0.0.0,instance=1,vrf=default}": 0, "frr_ospf_area_lsa_asbr_number{area=0.0.0.0,instance=1,vrf=default}": 0, "frr_ospf_area_lsa_nssa_number{area=0.0.0.0,instance=1,vrf=default}": 0, "frr_ospf_lsa_external_counter{instance=2,vrf=default}": 109, "frr_ospf_lsa_as_opaque_counter{instance=2,vrf=default}": 0, "frr_ospf_area_lsa_number{area=0.0.0.0,instance=2,vrf=default}": 17, "frr_ospf_area_lsa_network_number{area=0.0.0.0,instance=2,vrf=default}": 1, "frr_ospf_area_lsa_summary_number{area=0.0.0.0,instance=2,vrf=default}": 0, "frr_ospf_area_lsa_asbr_number{area=0.0.0.0,instance=2,vrf=default}": 0, "frr_ospf_area_lsa_nssa_number{area=0.0.0.0,instance=2,vrf=default}": 0, } runOSPFTest( t, "show_ip_ospf_vrf_all.json", processOSPF, getOSPFDesc, expected, ) } func TestProcessOSPFNeigh(t *testing.T) { expected := map[string]float64{ "frr_ospf_neighbor_state{iface=eth1,instance=1,local_address=192.168.4.2,neighbor=0.0.32.237,remote_address=192.168.4.3,vrf=default}": 4, "frr_ospf_neighbor_state{iface=eth0,instance=2,local_address=192.168.1.2,neighbor=0.0.35.148,remote_address=192.168.1.3,vrf=default}": 7, "frr_ospf_neighbor_state{iface=eth1,instance=2,local_address=192.168.2.2,neighbor=0.0.35.148,remote_address=192.168.2.3,vrf=default}": 4, "frr_ospf_neighbor_state{iface=eth0,instance=2,local_address=192.168.3.2,neighbor=0.0.32.237,remote_address=192.168.3.3,vrf=default}": 6, "frr_ospf_neighbor_state{iface=eth0,local_address=192.168.3.2,neighbor=0.0.32.237,remote_address=192.168.3.3,vrf=default}": 6, "frr_ospf_neighbor_state{iface=eth1,local_address=192.168.4.2,neighbor=0.0.32.237,remote_address=192.168.4.3,vrf=default}": 4, "frr_ospf_neighbor_state{iface=eth0,instance=1,local_address=192.168.1.2,neighbor=0.0.35.148,remote_address=192.168.1.3,vrf=default}": 7, "frr_ospf_neighbor_state{iface=eth0,instance=1,local_address=192.168.3.2,neighbor=0.0.32.237,remote_address=192.168.3.3,vrf=default}": 6, "frr_ospf_neighbor_state{iface=eth0,local_address=192.168.1.2,neighbor=0.0.35.148,remote_address=192.168.1.3,vrf=default}": 7, "frr_ospf_neighbor_state{iface=eth1,local_address=192.168.2.2,neighbor=0.0.35.148,remote_address=192.168.2.3,vrf=default}": 4, "frr_ospf_neighbor_state{iface=eth1,instance=1,local_address=192.168.2.2,neighbor=0.0.35.148,remote_address=192.168.2.3,vrf=default}": 4, "frr_ospf_neighbor_state{iface=eth1,instance=2,local_address=192.168.4.2,neighbor=0.0.32.237,remote_address=192.168.4.3,vrf=default}": 4, } runOSPFTest( t, "show_ip_ospf_vrf_all_neighbors.json", processOSPFNeigh, getOSPFNeighDesc, expected, ) } func TestProcessOSPFDataMaxAge(t *testing.T) { expected := map[string]float64{ "frr_ospf_data_ls_max_age{instance=default,vrf=2}": 2, "frr_ospf_data_ls_max_age{vrf=default}": 2, "frr_ospf_data_ls_max_age{instance=default,vrf=1}": 2, } runOSPFTest( t, "show_ip_ospf_vrf_all_database_max_age.json", processOSPFDataMaxAge, getOSPFDataMaxAgeDesc, expected, ) } prometheus-frr-exporter-1.11.0/collector/pim.go000066400000000000000000000057531516444222400215420ustar00rootroot00000000000000package collector import ( "encoding/json" "fmt" "log/slog" "strings" "github.com/prometheus/client_golang/prometheus" ) const pimSubsystem = "pim" func init() { registerCollector(pimSubsystem, disabledByDefault, NewPIMCollector) } type pimCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewPIMCollector collects PIM metrics, implemented as per the Collector interface. func NewPIMCollector(logger *slog.Logger) (Collector, error) { return &pimCollector{logger: logger, descriptions: getPIMDesc()}, nil } func getPIMDesc() map[string]*prometheus.Desc { labels := []string{"vrf"} neighborLabels := append(labels, "iface", "neighbor") return map[string]*prometheus.Desc{ "neighborCount": colPromDesc(pimSubsystem, "neighbor_count_total", "Number of neighbors detected", labels), "upTime": colPromDesc(pimSubsystem, "neighbor_uptime_seconds", "How long has the peer been up.", neighborLabels), } } // Collect implemented as per the Collector interface func (c *pimCollector) Update(ch chan<- prometheus.Metric) error { cmd := "show ip pim vrf all neighbor json" jsonPIMNeighbors, err := executePIMCommand(cmd) if err != nil { return err } if err := processPIMNeighbors(ch, jsonPIMNeighbors, c.logger, c.descriptions); err != nil { return cmdOutputProcessError(cmd, string(jsonPIMNeighbors), err) } return nil } func processPIMNeighbors(ch chan<- prometheus.Metric, jsonPIMNeighbors []byte, logger *slog.Logger, pimDesc map[string]*prometheus.Desc) error { var jsonMap map[string]json.RawMessage if err := json.Unmarshal(jsonPIMNeighbors, &jsonMap); err != nil { return fmt.Errorf("cannot unmarshal pim neighbors json: %s", err) } for vrfName, vrfData := range jsonMap { neighborCount := 0.0 var _tempvrfInstance map[string]json.RawMessage if err := json.Unmarshal(vrfData, &_tempvrfInstance); err != nil { return fmt.Errorf("cannot unmarshal VRF instance json: %s", err) } for ifaceName, ifaceData := range _tempvrfInstance { var neighbors map[string]pimNeighbor if err := json.Unmarshal(ifaceData, &neighbors); err != nil { return fmt.Errorf("cannot unmarshal neighbor json: %s", err) } for neighborIP, neighborData := range neighbors { neighborCount++ if uptimeSec, err := parseHMS(neighborData.UpTime); err != nil { logger.Error("cannot parse neighbor uptime", "uptime", neighborData.UpTime, "err", err) } else { // The labels are "vrf", "iface", "neighbor" neighborLabels := []string{strings.ToLower(vrfName), strings.ToLower(ifaceName), neighborIP} newGauge(ch, pimDesc["upTime"], float64(uptimeSec), neighborLabels...) } } } newGauge(ch, pimDesc["neighborCount"], neighborCount, vrfName) } return nil } func parseHMS(st string) (uint64, error) { var h, m, s uint64 n, err := fmt.Sscanf(st, "%d:%d:%d", &h, &m, &s) if err != nil || n != 3 { return 0, err } return h*3600 + m*60 + s, nil } type pimNeighbor struct { Interface string Neighbor string UpTime string } prometheus-frr-exporter-1.11.0/collector/pim_test.go000066400000000000000000000053761516444222400226020ustar00rootroot00000000000000package collector import ( "fmt" "regexp" "strings" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) var ( expectedPIMMetrics = map[string]float64{ "frr_pim_neighbor_uptime_seconds{iface=eth2,neighbor=192.0.2.227,vrf=red}": 13543, "frr_pim_neighbor_uptime_seconds{iface=eth1,neighbor=192.0.2.45,vrf=blue}": 13545, "frr_pim_neighbor_uptime_seconds{iface=eth0,neighbor=192.0.2.99,vrf=default}": 2745, "frr_pim_neighbor_count_total{vrf=red}": 1, "frr_pim_neighbor_count_total{vrf=blue}": 1, "frr_pim_neighbor_count_total{vrf=default}": 1, } parseHMStests = []struct { in string out uint64 }{ {"03:45:43", 13543}, {"00:04:01", 241}, {"10:00:43", 36043}, } ) func TestProcessPIMNeighbors(t *testing.T) { ch := make(chan prometheus.Metric, 1024) if err := processPIMNeighbors(ch, readTestFixture(t, "show_ip_pim_vrf_all_neighbor.json"), nil, getPIMDesc()); err != nil { t.Errorf("error calling processPIMNeighbors: %s", err) } close(ch) gotMetrics := make(map[string]float64) for { msg, more := <-ch if !more { break } metric := &dto.Metric{} if err := msg.Write(metric); err != nil { t.Errorf("error writing metric: %s", err) } var labels []string for _, label := range metric.GetLabel() { labels = append(labels, fmt.Sprintf("%s=%s", label.GetName(), label.GetValue())) } var value float64 if metric.GetCounter() != nil { value = metric.GetCounter().GetValue() } else if metric.GetGauge() != nil { value = metric.GetGauge().GetValue() } re, err := regexp.Compile(`.*fqName: "(.*)", help:.*`) if err != nil { t.Errorf("could not compile regex: %s", err) } metricName := re.FindStringSubmatch(msg.Desc().String())[1] gotMetrics[fmt.Sprintf("%s{%s}", metricName, strings.Join(labels, ","))] = value } for metricName, metricVal := range gotMetrics { if expectedMetricVal, ok := expectedPIMMetrics[metricName]; ok { if expectedMetricVal != metricVal { t.Errorf("metric %s expected value %v got %v", metricName, expectedMetricVal, metricVal) } } else { t.Errorf("unexpected metric: %s : %v", metricName, metricVal) } } for expectedMetricName, expectedMetricVal := range expectedPIMMetrics { if _, ok := gotMetrics[expectedMetricName]; !ok { t.Errorf("missing metric: %s value %v", expectedMetricName, expectedMetricVal) } } } func TestParseHMS(t *testing.T) { for _, tt := range parseHMStests { t.Run(tt.in, func(t *testing.T) { if uptimeSec, err := parseHMS(tt.in); err != nil || uptimeSec != tt.out { t.Errorf("ParseHMS => %s, got %d, wanted %d (err %s)", tt.in, uptimeSec, tt.out, err) } }) } } prometheus-frr-exporter-1.11.0/collector/route.go000066400000000000000000000115331516444222400221040ustar00rootroot00000000000000package collector import ( "encoding/json" "fmt" "log/slog" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" ) var ( routeSubsystem = "route" detailedRoutes = kingpin.Flag("collector.route.detailed-routes", "Enable detailed route count of each route type (default: disabled).").Default("False").Bool() ) func init() { registerCollector(routeSubsystem, enabledByDefault, NewRouteCollector) } type routeCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewRouteCollector collects route summary, implemented as per the Collector interface. func NewRouteCollector(logger *slog.Logger) (Collector, error) { return &routeCollector{logger: logger, descriptions: getRouteDesc()}, nil } func getRouteDesc() map[string]*prometheus.Desc { labels := []string{"afi", "route_type", "vrf"} totalLabels := []string{"afi", "vrf"} return map[string]*prometheus.Desc{ "total": colPromDesc(routeSubsystem, "total", "Total number of routes", totalLabels), "totalFib": colPromDesc(routeSubsystem, "total_fib", "Total number of routes in FIB", totalLabels), "fibCount": colPromDesc(routeSubsystem, "fib_count", "Number of routes of route type in FIB", labels), "fibOffloadedCount": colPromDesc(routeSubsystem, "fib_offloaded_count", "Number of offloaded routes of route type in FIB", labels), "fibTrappedCount": colPromDesc(routeSubsystem, "fib_trapped_count", "Number of trapped routes of route type in FIB", labels), "ribCount": colPromDesc(routeSubsystem, "rib_count", "Number of routes of route type in RIB", labels), } } // Update implemented as per the Collector interface. func (c *routeCollector) Update(ch chan<- prometheus.Metric) error { cmdIPv4 := "show ip route vrf all summary json" cmdIPv6 := "show ipv6 route vrf all summary json" jsonRouteIPv4, err := executeZebraCommand(cmdIPv4) if err != nil { return err } jsonRouteIPv6, err := executeZebraCommand(cmdIPv6) if err != nil { return err } if err := processRouteSummaries(ch, jsonRouteIPv4, "ipv4", c.descriptions); err != nil { return cmdOutputProcessError(cmdIPv4, string(jsonRouteIPv4), err) } if err := processRouteSummaries(ch, jsonRouteIPv6, "ipv6", c.descriptions); err != nil { return cmdOutputProcessError(cmdIPv6, string(jsonRouteIPv6), err) } return nil } func processRouteSummaries(ch chan<- prometheus.Metric, jsonRoute []byte, afi string, routeDesc map[string]*prometheus.Desc) error { var routeSummaries map[string]routeSummary if err := json.Unmarshal(jsonRoute, &routeSummaries); err != nil { // fallback for older FRR versions that do not return the VRF key var single routeSummary if err2 := json.Unmarshal(jsonRoute, &single); err2 != nil { // fallback for pre-10.1.0 FRR with multiple VRFs where "vrf all" // produces concatenated (invalid) JSON. Query each VRF individually. return processRouteSummariesPerVRF(ch, afi, routeDesc) } routeSummaries = map[string]routeSummary{ "default": single, } } for vrf, rs := range routeSummaries { emitRouteSummaryMetrics(ch, rs, afi, vrf, routeDesc) } return nil } func processRouteSummariesPerVRF(ch chan<- prometheus.Metric, afi string, routeDesc map[string]*prometheus.Desc) error { vrfs, err := getVRFs() if err != nil { return err } var cmdFmt string if afi == "ipv4" { cmdFmt = "show ip route vrf %s summary json" } else { cmdFmt = "show ipv6 route vrf %s summary json" } for _, vrf := range vrfs { cmd := fmt.Sprintf(cmdFmt, vrf) jsonRoute, err := executeZebraCommand(cmd) if err != nil { return err } var rs routeSummary if err := json.Unmarshal(jsonRoute, &rs); err != nil { return cmdOutputProcessError(cmd, string(jsonRoute), err) } emitRouteSummaryMetrics(ch, rs, afi, vrf, routeDesc) } return nil } func emitRouteSummaryMetrics(ch chan<- prometheus.Metric, rs routeSummary, afi string, vrf string, routeDesc map[string]*prometheus.Desc) { newGauge(ch, routeDesc["total"], float64(rs.RoutesTotal), afi, vrf) newGauge(ch, routeDesc["totalFib"], float64(rs.RoutesTotalFib), afi, vrf) if *detailedRoutes { for _, route := range rs.Routes { labels := []string{afi, route.Type, vrf} newGauge(ch, routeDesc["fibCount"], float64(route.Fib), labels...) newGauge(ch, routeDesc["fibOffloadedCount"], float64(route.FibOffLoaded), labels...) newGauge(ch, routeDesc["fibTrappedCount"], float64(route.FibTrapped), labels...) newGauge(ch, routeDesc["ribCount"], float64(route.Rib), labels...) } } } type routeSummary struct { Routes []route `json:"routes"` RoutesTotal uint32 `json:"routesTotal"` RoutesTotalFib uint32 `json:"routesTotalFib"` } type route struct { Fib uint32 `json:"fib"` Rib uint32 `json:"rib"` FibOffLoaded uint32 `json:"fibOffLoaded"` FibTrapped uint32 `json:"fibTrapped"` Type string `json:"type"` } prometheus-frr-exporter-1.11.0/collector/route_test.go000066400000000000000000000147671516444222400231570ustar00rootroot00000000000000package collector import ( "testing" "github.com/prometheus/client_golang/prometheus" ) var expectedRouteMetrics = map[string]float64{ "frr_route_fib_count{afi=ipv4,route_type=connected,vrf=default}": 1, "frr_route_fib_count{afi=ipv4,route_type=connected,vrf=red}": 2, "frr_route_fib_count{afi=ipv4,route_type=ebgp,vrf=red}": 1000504, "frr_route_fib_count{afi=ipv4,route_type=ibgp,vrf=red}": 0, "frr_route_fib_count{afi=ipv4,route_type=local,vrf=default}": 1, "frr_route_fib_count{afi=ipv4,route_type=local,vrf=red}": 2, "frr_route_fib_count{afi=ipv4,route_type=static,vrf=default}": 1, "frr_route_fib_count{afi=ipv4,route_type=static,vrf=red}": 3, "frr_route_fib_count{afi=ipv6,route_type=connected,vrf=default}": 2, "frr_route_fib_count{afi=ipv6,route_type=connected,vrf=red}": 2, "frr_route_fib_count{afi=ipv6,route_type=ebgp,vrf=red}": 218318, "frr_route_fib_count{afi=ipv6,route_type=ibgp,vrf=red}": 0, "frr_route_fib_count{afi=ipv6,route_type=local,vrf=red}": 1, "frr_route_fib_count{afi=ipv6,route_type=static,vrf=red}": 1, "frr_route_fib_offloaded_count{afi=ipv4,route_type=connected,vrf=default}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=connected,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=ebgp,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=ibgp,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=local,vrf=default}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=local,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=static,vrf=default}": 0, "frr_route_fib_offloaded_count{afi=ipv4,route_type=static,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=connected,vrf=default}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=connected,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=ebgp,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=ibgp,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=local,vrf=red}": 0, "frr_route_fib_offloaded_count{afi=ipv6,route_type=static,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=connected,vrf=default}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=connected,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=ebgp,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=ibgp,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=local,vrf=default}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=local,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=static,vrf=default}": 0, "frr_route_fib_trapped_count{afi=ipv4,route_type=static,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=connected,vrf=default}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=connected,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=ebgp,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=ibgp,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=local,vrf=red}": 0, "frr_route_fib_trapped_count{afi=ipv6,route_type=static,vrf=red}": 0, "frr_route_rib_count{afi=ipv4,route_type=connected,vrf=default}": 1, "frr_route_rib_count{afi=ipv4,route_type=connected,vrf=red}": 2, "frr_route_rib_count{afi=ipv4,route_type=ebgp,vrf=red}": 1000505, "frr_route_rib_count{afi=ipv4,route_type=ibgp,vrf=red}": 0, "frr_route_rib_count{afi=ipv4,route_type=local,vrf=default}": 1, "frr_route_rib_count{afi=ipv4,route_type=local,vrf=red}": 2, "frr_route_rib_count{afi=ipv4,route_type=static,vrf=default}": 1, "frr_route_rib_count{afi=ipv4,route_type=static,vrf=red}": 3, "frr_route_rib_count{afi=ipv6,route_type=connected,vrf=default}": 2, "frr_route_rib_count{afi=ipv6,route_type=connected,vrf=red}": 2, "frr_route_rib_count{afi=ipv6,route_type=ebgp,vrf=red}": 218319, "frr_route_rib_count{afi=ipv6,route_type=ibgp,vrf=red}": 0, "frr_route_rib_count{afi=ipv6,route_type=local,vrf=red}": 1, "frr_route_rib_count{afi=ipv6,route_type=static,vrf=red}": 1, "frr_route_total{afi=ipv4,vrf=default}": 3, "frr_route_total{afi=ipv4,vrf=red}": 1000512, "frr_route_total{afi=ipv6,vrf=default}": 2, "frr_route_total{afi=ipv6,vrf=red}": 218323, "frr_route_total_fib{afi=ipv4,vrf=default}": 3, "frr_route_total_fib{afi=ipv4,vrf=red}": 1000511, "frr_route_total_fib{afi=ipv6,vrf=default}": 2, "frr_route_total_fib{afi=ipv6,vrf=red}": 218322, } func TestParseVRFs(t *testing.T) { fixture := readTestFixture(t, "show_vrf.txt") got := parseVRFs(fixture) expected := []string{"default", "vrf-red", "vrf-blue"} if len(got) != len(expected) { t.Fatalf("expected %d VRFs, got %d: %v", len(expected), len(got), got) } for i, v := range expected { if got[i] != v { t.Errorf("expected VRF[%d] = %q, got %q", i, v, got[i]) } } } func TestParseVRFsEmpty(t *testing.T) { fixture := readTestFixture(t, "show_vrf_empty.txt") got := parseVRFs(fixture) if len(got) != 1 || got[0] != "default" { t.Errorf("expected [default], got %v", got) } } func TestProcessRouteSummaries(t *testing.T) { ch := make(chan prometheus.Metric, 1024) enableDetailedRoutes := true detailedRoutes = &enableDetailedRoutes jsonRouteIPv4 := readTestFixture(t, "show_ip_route_vrf_all_summary.json") if err := processRouteSummaries(ch, jsonRouteIPv4, "ipv4", getRouteDesc()); err != nil { t.Fatalf("error calling processRouteSummaries ipv4: %s", err) } jsonRouteIPv6 := readTestFixture(t, "show_ipv6_route_vrf_all_summary.json") if err := processRouteSummaries(ch, jsonRouteIPv6, "ipv6", getRouteDesc()); err != nil { t.Fatalf("error calling processRouteSummaries ipv6: %s", err) } close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expectedRouteMetrics) } prometheus-frr-exporter-1.11.0/collector/rpki.go000066400000000000000000000050671516444222400217200ustar00rootroot00000000000000package collector import ( "encoding/json" "fmt" "log/slog" "strconv" "github.com/prometheus/client_golang/prometheus" ) var rpkiSubsystem = "rpki" func init() { registerCollector(rpkiSubsystem, disabledByDefault, NewRPKICollector) } type rpkiCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewRPKICollector collects RPKI cache-connection metrics, implemented as per the Collector interface. func NewRPKICollector(logger *slog.Logger) (Collector, error) { return &rpkiCollector{logger: logger, descriptions: getRPKIDesc()}, nil } func getRPKIDesc() map[string]*prometheus.Desc { labels := []string{"vrf", "mode", "host", "port"} return map[string]*prometheus.Desc{ "cacheState": colPromDesc(rpkiSubsystem, "cache_state", "State of the RPKI cache connection (1 = connected, 0 = disconnected).", labels), "cachePreference": colPromDesc(rpkiSubsystem, "cache_preference", "Preference value of the RPKI cache connection.", labels), } } // Update implemented as per the Collector interface. func (c *rpkiCollector) Update(ch chan<- prometheus.Metric) error { vrfs, err := getVRFs() if err != nil { return err } for _, vrf := range vrfs { var cmd string if vrf == "default" { cmd = "show rpki cache-connection json" } else { cmd = fmt.Sprintf("show rpki cache-connection vrf %s json", vrf) } output, err := executeBGPCommand(cmd) if err != nil { return err } if len(output) == 0 { continue } if err := processRPKICacheConnection(ch, output, vrf, c.descriptions); err != nil { return cmdOutputProcessError(cmd, string(output), err) } } return nil } func processRPKICacheConnection(ch chan<- prometheus.Metric, jsonRPKI []byte, vrf string, rpkiDesc map[string]*prometheus.Desc) error { var cacheConn rpkiCacheConnection if err := json.Unmarshal(jsonRPKI, &cacheConn); err != nil { return err } for _, conn := range cacheConn.Connections { labels := []string{vrf, conn.Mode, conn.Host, strconv.Itoa(conn.Port)} state := 0.0 if conn.State == "connected" { state = 1.0 } newGauge(ch, rpkiDesc["cacheState"], state, labels...) newGauge(ch, rpkiDesc["cachePreference"], float64(conn.Preference), labels...) } return nil } type rpkiCacheConnection struct { ConnectedGroup int `json:"connectedGroup"` Connections []rpkiConnection `json:"connections"` } type rpkiConnection struct { Mode string `json:"mode"` Host string `json:"host"` Port int `json:"port,string"` Preference int `json:"preference"` State string `json:"state"` } prometheus-frr-exporter-1.11.0/collector/rpki_test.go000066400000000000000000000027441516444222400227560ustar00rootroot00000000000000package collector import ( "testing" "github.com/prometheus/client_golang/prometheus" ) var expectedRPKIMetrics = map[string]float64{ "frr_rpki_cache_state{host=172.20.15.59,mode=tcp,port=8082,vrf=default}": 1, "frr_rpki_cache_preference{host=172.20.15.59,mode=tcp,port=8082,vrf=default}": 10, "frr_rpki_cache_state{host=172.20.15.60,mode=tcp,port=8083,vrf=default}": 0, "frr_rpki_cache_preference{host=172.20.15.60,mode=tcp,port=8083,vrf=default}": 20, } func TestProcessRPKICacheConnection(t *testing.T) { ch := make(chan prometheus.Metric, 1024) if err := processRPKICacheConnection(ch, readTestFixture(t, "show_rpki_cache_connection.json"), "default", getRPKIDesc()); err != nil { t.Errorf("error calling processRPKICacheConnection: %s", err) } close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expectedRPKIMetrics) } var expectedRPKIVRFMetrics = map[string]float64{ "frr_rpki_cache_state{host=172.20.15.59,mode=tcp,port=8082,vrf=TEST}": 1, "frr_rpki_cache_preference{host=172.20.15.59,mode=tcp,port=8082,vrf=TEST}": 10, } func TestProcessRPKICacheConnectionVRF(t *testing.T) { ch := make(chan prometheus.Metric, 1024) if err := processRPKICacheConnection(ch, readTestFixture(t, "show_rpki_cache_connection_vrf_TEST.json"), "TEST", getRPKIDesc()); err != nil { t.Errorf("error calling processRPKICacheConnection VRF: %s", err) } close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expectedRPKIVRFMetrics) } prometheus-frr-exporter-1.11.0/collector/status.go000066400000000000000000000046471516444222400223010ustar00rootroot00000000000000package collector import ( "fmt" "log/slog" "regexp" "strings" "github.com/prometheus/client_golang/prometheus" ) const statusSubsystem = "status" func init() { registerCollector(statusSubsystem, enabledByDefault, NewStatusCollector) } type statusCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewStatusCollector collects FRR status metrics, implemented as per the Collector interface. func NewStatusCollector(logger *slog.Logger) (Collector, error) { return &statusCollector{logger: logger, descriptions: getStatusDesc()}, nil } func getStatusDesc() map[string]*prometheus.Desc { labels := []string{"version", "os"} return map[string]*prometheus.Desc{ "up": colPromDesc(statusSubsystem, "up", "FRR status (1 = up and responding, 0 = down or unreachable)", labels), } } // Update implemented as per the Collector interface func (c *statusCollector) Update(ch chan<- prometheus.Metric) error { cmd := "show version" output, err := executeZebraCommand(cmd) var version, os string var status float64 if err != nil { c.logger.Error("failed to execute show version command", "err", err) version = "unknown" os = "unknown" status = 0 } else { var parseErr error version, os, parseErr = processStatusVersion(output) if parseErr != nil { c.logger.Error("failed to parse show version output", "err", parseErr, "output", string(output)) version = "unknown" os = "unknown" status = 0 } else { status = 1 } } newGauge(ch, c.descriptions["up"], status, version, os) // Always return nil - we always emit a metric to indicate status return nil } func processStatusVersion(output []byte) (string, string, error) { text := string(output) lines := strings.Split(text, "\n") if len(lines) == 0 { return "", "", fmt.Errorf("empty output") } firstLine := lines[0] // Extract version using regex: FRRouting VERSION (...) versionRegex := regexp.MustCompile(`FRRouting (\S+)`) versionMatch := versionRegex.FindStringSubmatch(firstLine) if len(versionMatch) < 2 { return "", "", fmt.Errorf("could not extract version from: %s", firstLine) } version := versionMatch[1] // Extract OS using regex: on OS. OS is optional as not all FRR // distributions include it (e.g. Cumulus). var os string osRegex := regexp.MustCompile(`on (.+)\.$`) osMatch := osRegex.FindStringSubmatch(firstLine) if len(osMatch) >= 2 { os = osMatch[1] } return version, os, nil } prometheus-frr-exporter-1.11.0/collector/status_test.go000066400000000000000000000056411516444222400233330ustar00rootroot00000000000000package collector import ( "testing" "github.com/prometheus/client_golang/prometheus" ) var ( expectedStatusMetrics = map[string]float64{ "frr_status_up{os=Linux(5.14.0-284.11.1.el9_2.x86_64),version=10.3.1}": 1, } expectedStatusMetricsDown = map[string]float64{ "frr_status_up{os=unknown,version=unknown}": 0, } ) func TestProcessStatusVersion(t *testing.T) { fixture := readTestFixture(t, "show_version.txt") version, os, err := processStatusVersion(fixture) if err != nil { t.Errorf("error calling processStatusVersion: %s", err) } expectedVersion := "10.3.1" if version != expectedVersion { t.Errorf("expected version %s, got %s", expectedVersion, version) } expectedOS := "Linux(5.14.0-284.11.1.el9_2.x86_64)" if os != expectedOS { t.Errorf("expected os %s, got %s", expectedOS, os) } } func TestProcessStatusVersionGit(t *testing.T) { fixture := readTestFixture(t, "show_version_git.txt") version, os, err := processStatusVersion(fixture) if err != nil { t.Errorf("error calling processStatusVersion: %s", err) } expectedVersion := "9.1_git" if version != expectedVersion { t.Errorf("expected version %s, got %s", expectedVersion, version) } expectedOS := "Linux(6.12.43-talos)" if os != expectedOS { t.Errorf("expected os %s, got %s", expectedOS, os) } } func TestProcessStatusVersionWithMetrics(t *testing.T) { fixture := readTestFixture(t, "show_version.txt") version, os, err := processStatusVersion(fixture) if err != nil { t.Errorf("error calling processStatusVersion: %s", err) } ch := make(chan prometheus.Metric, 1024) statusDesc := getStatusDesc() newGauge(ch, statusDesc["up"], 1, version, os) close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expectedStatusMetrics) } func TestProcessStatusVersionNoOS(t *testing.T) { fixture := readTestFixture(t, "show_version_no_os.txt") version, os, err := processStatusVersion(fixture) if err != nil { t.Errorf("error calling processStatusVersion: %s", err) } expectedVersion := "7.5+cl5.2.0u0" if version != expectedVersion { t.Errorf("expected version %s, got %s", expectedVersion, version) } if os != "" { t.Errorf("expected empty os, got %s", os) } } func TestProcessStatusVersionEmpty(t *testing.T) { _, _, err := processStatusVersion([]byte("")) if err == nil { t.Error("expected error for empty output, got nil") } } func TestProcessStatusVersionInvalid(t *testing.T) { _, _, err := processStatusVersion([]byte("Invalid output without version info")) if err == nil { t.Error("expected error for invalid output, got nil") } } func TestStatusCollectorUpdateFailure(t *testing.T) { ch := make(chan prometheus.Metric, 1024) statusDesc := getStatusDesc() // Simulate failure case by emitting status=0 with unknown labels newGauge(ch, statusDesc["up"], 0, "unknown", "unknown") close(ch) gotMetrics := collectMetrics(t, ch) compareMetrics(t, gotMetrics, expectedStatusMetricsDown) } prometheus-frr-exporter-1.11.0/collector/testdata/000077500000000000000000000000001516444222400222255ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/collector/testdata/prefix_filter.txt000066400000000000000000000001141516444222400256240ustar00rootroot00000000000000# This is a comment 10.0.0.0/24 10.0.1.0/24 # Another comment fd00::/64 prometheus-frr-exporter-1.11.0/collector/testdata/prefix_filter_invalid.txt000066400000000000000000000000311516444222400273300ustar00rootroot0000000000000010.0.0.0/24 not-a-prefix prometheus-frr-exporter-1.11.0/collector/testdata/show_bfd_peers.json000066400000000000000000000025571516444222400261220ustar00rootroot00000000000000[ { "multihop": false, "peer": "10.10.141.61", "local": "10.10.141.81", "interface": "eth0", "vrf": "default", "id": 869087474, "remote-id": 533345668, "status": "up", "uptime": 847716, "diagnostic": "ok", "remote-diagnostic": "ok", "receive-interval": 300, "transmit-interval": 300, "echo-interval": 0, "remote-receive-interval": 300, "remote-transmit-interval": 300, "remote-echo-interval": 300 }, { "multihop": false, "peer": "10.10.141.62", "local": "10.10.141.81", "interface": "eth1", "vrf": "blue", "id": 2809641312, "remote-id": 3617154307, "status": "up", "uptime": 847595, "diagnostic": "ok", "remote-diagnostic": "ok", "receive-interval": 300, "transmit-interval": 300, "echo-interval": 0, "remote-receive-interval": 300, "remote-transmit-interval": 300, "remote-echo-interval": 300 }, { "multihop": false, "peer": "10.10.141.63", "local": "10.10.141.81", "vrf": "default", "id": 2809641312, "remote-id": 3617154307, "status": "down", "uptime": 847888, "diagnostic": "ok", "remote-diagnostic": "ok", "receive-interval": 300, "transmit-interval": 300, "echo-interval": 0, "remote-receive-interval": 300, "remote-transmit-interval": 300, "remote-echo-interval": 300 } ] show_bgp_ipv4_unicast_neighbors_advertised_routes.json000066400000000000000000000003321516444222400352120ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/collector/testdata{ "advertisedRoutes": { "10.0.0.0/24": { "valid": true, "network": "10.0.0.0/24" }, "10.0.3.0/24": { "valid": true, "network": "10.0.3.0/24" } }, "totalPrefixCounter": 2 } prometheus-frr-exporter-1.11.0/collector/testdata/show_bgp_ipv4_unicast_neighbors_routes.json000066400000000000000000000010611516444222400330570ustar00rootroot00000000000000{ "routes": { "10.0.0.0/24": [ { "valid": true, "bestpath": true, "network": "10.0.0.0/24", "nexthops": [{"ip": "192.168.0.2"}] } ], "10.0.1.0/24": [ { "valid": true, "bestpath": true, "network": "10.0.1.0/24", "nexthops": [{"ip": "192.168.0.2"}] } ], "10.0.2.0/24": [ { "valid": true, "bestpath": true, "network": "10.0.2.0/24", "nexthops": [{"ip": "192.168.0.2"}] } ] }, "totalPrefixCounter": 3 } prometheus-frr-exporter-1.11.0/collector/testdata/show_bgp_vrf_all_ipv4_summary.json000066400000000000000000000050671516444222400311640ustar00rootroot00000000000000{ "default": { "ipv4Unicast": { "routerId": "192.168.0.1", "as": 64512, "vrfId": 0, "vrfName": "default", "tableVersion": 0, "ribCount": 1, "ribMemory": 64, "peerCount": 2, "peerMemory": 39936, "peers": { "192.168.0.2": { "remoteAs": 64513, "version": 4, "msgRcvd": 100, "msgSent": 100, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "10000", "peerUptimeMsec": 10000, "prefixReceivedCount": 0, "state": "Established", "idType": "ipv4" }, "192.168.0.3": { "remoteAs": 64514, "version": 4, "msgRcvd": 0, "msgSent": 0, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "never", "peerUptimeMsec": 0, "pfxRcd": 2, "state": "Active", "idType": "ipv4" }, "192.168.0.4": { "remoteAs": 64515, "version": 4, "msgRcvd": 0, "msgSent": 0, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "never", "peerUptimeMsec": 0, "pfxRcd": 2, "state": "Idle (Admin)", "idType": "ipv4" } }, "totalPeers": 2, "dynamicPeers": 0, "bestPath": { "multiPathRelax": "false" } } }, "red": { "ipv4Unicast": { "routerId": "192.168.1.1", "as": 64612, "vrfId": 39, "vrfName": "red", "tableVersion": 0, "ribCount": 0, "ribMemory": 0, "peerCount": 2, "peerMemory": 39936, "peers": { "192.168.1.2": { "remoteAs": 64613, "version": 4, "msgRcvd": 100, "msgSent": 100, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "10000", "peerUptimeMsec": 20000, "prefixReceivedCount": 2, "state": "Established", "idType": "ipv4" }, "192.168.1.3": { "remoteAs": 64614, "version": 4, "msgRcvd": 200, "msgSent": 200, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "never", "peerUptimeMsec": 0, "prefixReceivedCount": 0, "state": "Active", "idType": "ipv4" } }, "totalPeers": 2, "dynamicPeers": 0, "bestPath": { "multiPathRelax": "false" } } } }prometheus-frr-exporter-1.11.0/collector/testdata/show_bgp_vrf_all_ipv6_summary.json000066400000000000000000000043571516444222400311670ustar00rootroot00000000000000{ "default": { "ipv6Unicast": { "routerId": "192.168.0.1", "as": 64512, "vrfId": 0, "vrfName": "default", "tableVersion": 6, "ribCount": 3, "ribMemory": 456, "peerCount": 2, "peerMemory": 59904, "peers": { "fd00::1": { "remoteAs": 64513, "version": 4, "msgRcvd": 29285, "msgSent": 29285, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "1d00h24m", "peerUptimeMsec": 8465643000000, "prefixReceivedCount": 1, "state": "Established", "idType": "ipv6" }, "fd00::5": { "remoteAs": 64514, "version": 4, "msgRcvd": 0, "msgSent": 0, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "never", "peerUptimeMsec": 0, "prefixReceivedCount": 0, "state": "Active", "idType": "ipv6" } }, "totalPeers": 2, "dynamicPeers": 0, "bestPath": { "multiPathRelax": "false" } } }, "red": { "ipv6Unicast": { "routerId": "192.168.1.1", "as": 64612, "vrfId": 0, "vrfName": "default", "tableVersion": 6, "ribCount": 3, "ribMemory": 456, "peerCount": 2, "peerMemory": 59904, "peers": { "fd00::101": { "remoteAs": 64613, "version": 4, "msgRcvd": 29285, "msgSent": 29285, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "1d00h24m", "peerUptimeMsec": 87873000, "prefixReceivedCount": 1, "state": "Established", "idType": "ipv6" }, "fd00::105": { "remoteAs": 64614, "version": 4, "msgRcvd": 0, "msgSent": 0, "tableVersion": 0, "outq": 0, "inq": 0, "peerUptime": "never", "peerUptimeMsec": 0, "prefixReceivedCount": 0, "state": "Active", "idType": "ipv6" } }, "totalPeers": 2, "dynamicPeers": 0, "bestPath": { "multiPathRelax": "false" } } } }prometheus-frr-exporter-1.11.0/collector/testdata/show_bgp_vrf_all_neighbors.json000066400000000000000000000004511516444222400304750ustar00rootroot00000000000000{ "default":{ "vrfId":0, "vrfName":"default", "swp2":{ "nbrDesc":"{\"desc\":\"fw1\"}" }, "10.1.1.10":{ "nbrDesc":"{\"desc\":\"rt1\"}" } }, "vrf1":{ "vrfId":-1, "vrfName":"vrf1", "10.2.0.1":{ "nbrDesc":"{\"desc\":\"remote\"}" } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_evpn_vni.json000066400000000000000000000006661516444222400260140ustar00rootroot00000000000000{ "174374":{ "vni":174374, "type":"L2", "vxlanIf":"ONTEP1_174374", "numMacs":42, "numArpNd":0, "numRemoteVteps":1, "tenantVrf":"default", "remoteVteps":[ "10.0.0.13" ] }, "172192":{ "vni":172192, "type":"L2", "vxlanIf":"ONTEP1_172192", "numMacs":0, "numArpNd":23, "numRemoteVteps":"n\/a", "tenantVrf":"default", "remoteVteps":[ "10.0.0.13" ] } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_bgp_vrf_all_nexthop.json000066400000000000000000000015031516444222400306710ustar00rootroot00000000000000{ "default":{ "ipv4":{ "10.1.2.1":{ "valid":true, "complete":true, "igpMetric":0, "pathCount":0, "peer":"10.1.2.1", "resolvedPrefix":"10.1.2.0/24", "nexthops":[ { "interfaceName":"eth1" } ], "lastUpdate":{ "epoch":1750342037, "string":"Thu Jun 19 14:07:17 2025\n" } }, "10.2.2.1":{ "valid":true, "complete":true, "igpMetric":0, "pathCount":0, "peer":"10.2.2.1", "resolvedPrefix":"10.2.2.0/24", "nexthops":[ { "interfaceName":"eth2" } ], "lastUpdate":{ "epoch":1750342037, "string":"Thu Jun 19 14:07:17 2025\n" } } }, "ipv6":{} } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_ospf_vrf_all.json000066400000000000000000000033761516444222400273350ustar00rootroot00000000000000{ "default": { "vrfName": "default", "vrfId": 0, "routerId": "0.0.56.137", "tosRoutesOnly": true, "rfc2328Conform": true, "rfc1583Compatibility": true, "spfScheduleDelayMsecs": 2000, "holdtimeMinMsecs": 5000, "holdtimeMaxMsecs": 20000, "holdtimeMultplier": 1, "spfLastExecutedMsecs": 492410115, "spfLastDurationMsecs": 0, "lsaMinIntervalMsecs": 5000, "lsaMinArrivalMsecs": 5000, "writeMultiplier": 20, "refreshTimerMsecs": 10000, "maximumPaths": 256, "preference": 110, "asbrRouter": "injectingExternalRoutingInformation", "lsaExternalCounter": 109, "lsaExternalChecksum": 3680599, "lsaAsopaqueCounter": 0, "lsaAsOpaqueChecksum": 0, "attachedAreaCounter": 1, "areas": { "0.0.0.0": { "backbone": true, "areaIfTotalCounter": 7, "areaIfActiveCounter": 7, "nbrFullAdjacentCounter": 3, "authentication": "authenticationNone", "spfExecutedCounter": 43, "lsaNumber": 17, "lsaRouterNumber": 16, "lsaRouterChecksum": 539897, "lsaNetworkNumber": 1, "lsaNetworkChecksum": 6844, "lsaSummaryNumber": 0, "lsaSummaryChecksum": 0, "lsaAsbrNumber": 0, "lsaAsbrChecksum": 0, "lsaNssaNumber": 0, "lsaNssaChecksum": 0, "lsaOpaqueLinkNumber": 0, "lsaOpaqueLinkChecksum": 0, "lsaOpaqueAreaNumber": 0, "lsaOpaqueAreaChecksum": 0 } } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_ospf_vrf_all_database_max_age.json000066400000000000000000000002661516444222400326350ustar00rootroot00000000000000{ "default":{ "vrfName":"default", "vrfId":0, "routerId":"0.2.81.82", "maxAgeLinkStates":{ "10.1.0.0":{}, "10.2.0.0":{} } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_ospf_vrf_all_interface.json000066400000000000000000000074111516444222400313470ustar00rootroot00000000000000{ "default":{ "vrfName":"default", "vrfId":0, "swp1":{ "ifUp":true, "ifIndex":4, "mtuBytes":1500, "bandwidthMbit":4294967295, "ifFlags":"", "ospfEnabled":true, "ipAddress":"192.168.0.1", "ipAddressPrefixlen":24, "area":"0.0.0.0", "routerId":"192.168.255.1", "networkType":"BROADCAST", "cost":1, "transmitDelayMsecs":1000, "state":"DR", "priority":1, "mcastMemberOspfAllRouters":true, "mcastMemberOspfDesignatedRouters":true, "timerMsecs":100, "timerDeadMsecs":25, "timerWaitMsecs":25, "timerRetransmit":200, "timerHelloInMsecs":7769, "nbrCount":0, "nbrAdjacentCount":0 }, "swp2":{ "ifUp":true, "ifIndex":6, "mtuBytes":1500, "bandwidthMbit":4294967295, "ifFlags":"", "ospfEnabled":true, "ipAddress":"192.168.2.1", "ipAddressPrefixlen":24, "area":"0.0.0.0", "routerId":"192.168.255.1", "networkType":"BROADCAST", "cost":1, "transmitDelayMsecs":1000, "state":"DR", "priority":1, "bdrId":"1.1.1.1", "bdrAddress":"192.168.1.2", "networkLsaSequence":2147483717, "mcastMemberOspfAllRouters":true, "mcastMemberOspfDesignatedRouters":true, "timerMsecs":100, "timerDeadMsecs":25, "timerWaitMsecs":25, "timerRetransmit":200, "timerHelloInMsecs":7769, "nbrCount":1, "nbrAdjacentCount":1 } }, "red":{ "vrfName":"red", "vrfId":0, "swp3":{ "ifUp":true, "ifIndex":4, "mtuBytes":1500, "bandwidthMbit":4294967295, "ifFlags":"", "ospfEnabled":true, "ipAddress":"192.168.10.1", "ipAddressPrefixlen":24, "area":"0.0.0.0", "routerId":"192.168.255.1", "networkType":"BROADCAST", "cost":1, "transmitDelayMsecs":1000, "state":"DR", "priority":1, "mcastMemberOspfAllRouters":true, "mcastMemberOspfDesignatedRouters":true, "timerMsecs":100, "timerDeadMsecs":25, "timerWaitMsecs":25, "timerRetransmit":200, "timerHelloInMsecs":7769, "nbrCount":0, "nbrAdjacentCount":0 }, "swp4":{ "ifUp":true, "ifIndex":6, "mtuBytes":1500, "bandwidthMbit":4294967295, "ifFlags":"", "ospfEnabled":true, "ipAddress":"192.168.12.1", "ipAddressPrefixlen":24, "area":"0.0.0.0", "routerId":"192.168.255.1", "networkType":"BROADCAST", "cost":1, "transmitDelayMsecs":1000, "state":"DR", "priority":1, "bdrId":"1.1.1.1", "bdrAddress":"192.168.1.2", "networkLsaSequence":2147483717, "mcastMemberOspfAllRouters":true, "mcastMemberOspfDesignatedRouters":true, "timerMsecs":100, "timerDeadMsecs":25, "timerWaitMsecs":25, "timerRetransmit":200, "timerHelloInMsecs":7769, "nbrCount":1, "nbrAdjacentCount":1 }, "peerlink.4094":{ "ifUp":true, "ifIndex":62, "mtuBytes":9000, "bandwidthMbit":2000, "ifFlags":"", "ospfEnabled":true, "ipAddress":"169.254.1.1", "ipAddressPrefixlen":30, "ospfIfType":"Broadcast", "localIfUsed":"169.254.1.3", "area":"0.0.0.75 [Stub]", "routerId":"10.200.1.222", "networkType":"BROADCAST", "cost":50, "transmitDelaySecs":1, "state":"DR", "priority":1, "timerMsecs":10000, "timerDeadSecs":40, "timerWaitSecs":40, "timerRetransmitSecs":5, "timerPassiveIface":true, "nbrCount":0, "nbrAdjacentCount":0 } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_ospf_vrf_all_neighbors.json000066400000000000000000000020441516444222400313640ustar00rootroot00000000000000{ "default": { "vrfName": "default", "vrfId": 0, "neighbors": { "0.0.35.148": [ { "nbrState": "Full/-", "role": "DROther", "ifaceName": "eth0:192.168.1.2", "address": "192.168.1.3" }, { "state": "ExStart/-", "role": "DROther", "ifaceName": "eth1:192.168.2.2", "address": "192.168.2.3" } ], "0.0.32.237": [ { "state": "Loading/-", "role": "DROther", "ifaceName": "eth0:192.168.3.2", "address": "192.168.3.3" }, { "nbrState": "ExStart/-", "role": "DROther", "ifaceName": "eth1:192.168.4.2", "address": "192.168.4.3" } ] } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_pim_vrf_all_neighbor.json000066400000000000000000000013501516444222400310160ustar00rootroot00000000000000{ "red": { "red":{}, "eth2":{ "192.0.2.227":{ "interface":"eth2", "neighbor":"192.0.2.227", "upTime":"03:45:43", "holdTime":"00:01:43", "holdTimeMax":105, "drPriority":1 } } }, "blue": { "blue":{}, "eth1":{ "192.0.2.45":{ "interface":"eth1", "neighbor":"192.0.2.45", "upTime":"03:45:45", "holdTime":"00:01:34", "holdTimeMax":105, "drPriority":1 } } }, "default": { "eth0":{ "192.0.2.99":{ "interface":"eth1", "neighbor":"192.0.2.99", "upTime":"00:45:45", "holdTime":"00:02:34", "holdTimeMax":105, "drPriority":1 } } } } prometheus-frr-exporter-1.11.0/collector/testdata/show_ip_route_vrf_all_summary.json000066400000000000000000000024601516444222400312720ustar00rootroot00000000000000{ "default":{ "routes":[ { "fib":1, "rib":1, "fibOffLoaded":0, "fibTrapped":0, "type":"connected" }, { "fib":1, "rib":1, "fibOffLoaded":0, "fibTrapped":0, "type":"local" }, { "fib":1, "rib":1, "fibOffLoaded":0, "fibTrapped":0, "type":"static" } ], "routesTotal":3, "routesTotalFib":3 }, "red":{ "routes":[ { "fib":2, "rib":2, "fibOffLoaded":0, "fibTrapped":0, "type":"connected" }, { "fib":2, "rib":2, "fibOffLoaded":0, "fibTrapped":0, "type":"local" }, { "fib":3, "rib":3, "fibOffLoaded":0, "fibTrapped":0, "type":"static" }, { "fib":1000504, "rib":1000505, "fibOffLoaded":0, "fibTrapped":0, "type":"ebgp" }, { "fib":0, "rib":0, "fibOffLoaded":0, "fibTrapped":0, "type":"ibgp" } ], "routesTotal":1000512, "routesTotalFib":1000511 } }prometheus-frr-exporter-1.11.0/collector/testdata/show_ipv6_route_vrf_all_summary.json000066400000000000000000000020271516444222400315450ustar00rootroot00000000000000{ "default":{ "routes":[ { "fib":2, "rib":2, "fibOffLoaded":0, "fibTrapped":0, "type":"connected" } ], "routesTotal":2, "routesTotalFib":2 }, "red":{ "routes":[ { "fib":2, "rib":2, "fibOffLoaded":0, "fibTrapped":0, "type":"connected" }, { "fib":1, "rib":1, "fibOffLoaded":0, "fibTrapped":0, "type":"local" }, { "fib":1, "rib":1, "fibOffLoaded":0, "fibTrapped":0, "type":"static" }, { "fib":218318, "rib":218319, "fibOffLoaded":0, "fibTrapped":0, "type":"ebgp" }, { "fib":0, "rib":0, "fibOffLoaded":0, "fibTrapped":0, "type":"ibgp" } ], "routesTotal":218323, "routesTotalFib":218322 } }prometheus-frr-exporter-1.11.0/collector/testdata/show_rpki_cache_connection.json000066400000000000000000000004731516444222400304730ustar00rootroot00000000000000{ "connectedGroup":10, "connections":[ { "mode":"tcp", "host":"172.20.15.59", "port":"8082", "preference":10, "state":"connected" }, { "mode":"tcp", "host":"172.20.15.60", "port":"8083", "preference":20, "state":"disconnected" } ] } prometheus-frr-exporter-1.11.0/collector/testdata/show_rpki_cache_connection_vrf_TEST.json000066400000000000000000000002641516444222400322050ustar00rootroot00000000000000{ "connectedGroup":10, "connections":[ { "mode":"tcp", "host":"172.20.15.59", "port":"8082", "preference":10, "state":"connected" } ] } prometheus-frr-exporter-1.11.0/collector/testdata/show_version.txt000066400000000000000000000024251516444222400255160ustar00rootroot00000000000000FRRouting 10.3.1 (router) on Linux(5.14.0-284.11.1.el9_2.x86_64). Copyright 1996-2005 Kunihiro Ishiguro, et al. configured with: '--build=x86_64-redhat-linux-gnu' '--host=x86_64-redhat-linux-gnu' '--program-prefix=' '--disable-dependency-tracking' '--prefix=/usr' '--exec-prefix=/usr' '--bindir=/usr/bin' '--datadir=/usr/share' '--includedir=/usr/include' '--libdir=/usr/lib64' '--libexecdir=/usr/libexec' '--sharedstatedir=/var/lib' '--mandir=/usr/share/man' '--infodir=/usr/share/info' '--sbindir=/usr/lib/frr' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-static' '--disable-werror' '--enable-multipath=256' '--enable-vtysh' '--enable-ospfclient' '--enable-ospfapi' '--enable-rtadv' '--enable-ldpd' '--enable-pimd' '--enable-pim6d' '--enable-pbrd' '--enable-nhrpd' '--enable-eigrpd' '--enable-babeld' '--enable-vrrpd' '--enable-user=frr' '--enable-group=frr' '--enable-vty-group=frrvty' '--enable-fpm' '--enable-watchfrr' '--disable-bgp-vnc' '--enable-isisd' '--enable-doc' '--enable-rpki' '--enable-bfdd' '--enable-pathd' '--disable-grpc' '--enable-snmp' '--disable-zeromq' '--enable-pcre2posix' 'build_alias=x86_64-redhat-linux-gnu' 'host_alias=x86_64-redhat-linux-gnu' 'PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig' 'CC=gcc' 'CXX=g++' 'LT_SYS_LIBRARY_PATH=/usr/lib64:' prometheus-frr-exporter-1.11.0/collector/testdata/show_version_git.txt000066400000000000000000000006531516444222400263620ustar00rootroot00000000000000FRRouting 9.1_git (router) on Linux(6.12.43-talos). Copyright 1996-2005 Kunihiro Ishiguro, et al. configured with: '--prefix=/usr' '--sbindir=/usr/lib/frr' '--sysconfdir=/etc/frr' '--libdir=/usr/lib' '--localstatedir=/var/run/frr' '--enable-rpki' '--enable-vtysh' '--enable-multipath=64' '--enable-vty-group=frrvty' '--enable-user=frr' '--enable-group=frr' '--enable-pcre2posix' '--enable-scripting' 'CC=gcc' 'CXX=g++'\n prometheus-frr-exporter-1.11.0/collector/testdata/show_version_no_os.txt000066400000000000000000000001201516444222400267010ustar00rootroot00000000000000FRRouting 7.5+cl5.2.0u0 (router). Copyright 1996-2005 Kunihiro Ishiguro, et al. prometheus-frr-exporter-1.11.0/collector/testdata/show_vrf.txt000066400000000000000000000001171516444222400246220ustar00rootroot00000000000000vrf vrf-red id 4 table 10 (configured) vrf vrf-blue id 5 table 11 (configured) prometheus-frr-exporter-1.11.0/collector/testdata/show_vrf_empty.txt000066400000000000000000000000001516444222400260270ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/collector/testdata/show_vrrp.json000066400000000000000000000040401516444222400251470ustar00rootroot00000000000000[ { "vrid":1, "version":3, "autoconfigured":false, "shutdown":false, "preemptMode":true, "acceptMode":true, "interface":"gw_extnet", "advertisementInterval":1000, "v4":{ "interface":"extnet_v4_1", "vmac":"00:00:5e:00:01:01", "primaryAddress":"", "status":"Backup", "effectivePriority":100, "masterAdverInterval":1000, "skewTime":600, "masterDownInterval":3600, "stats":{ "adverTx":6, "adverRx":1548196, "garpTx":4, "transitions":9 }, "addresses":[ "192.0.2.1" ] }, "v6":{ "interface":"extnet_v6_1", "vmac":"00:00:5e:00:02:01", "primaryAddress":"::", "status":"Backup", "effectivePriority":100, "masterAdverInterval":1000, "skewTime":600, "masterDownInterval":3600, "stats":{ "adverTx":2, "adverRx":1548195, "neighborAdverTx":5, "transitions":11 }, "addresses":[ "2001:DB8:2c02::1" ] } }, { "vrid":2, "version":3, "autoconfigured":false, "shutdown":false, "preemptMode":true, "acceptMode":true, "interface":"gw_extnet", "advertisementInterval":1000, "v4":{ "interface":"extnet_v4_2", "vmac":"00:00:5e:00:01:02", "primaryAddress":"192.0.2.3", "status":"Master", "effectivePriority":200, "masterAdverInterval":1000, "skewTime":210, "masterDownInterval":3210, "stats":{ "adverTx":1548210, "adverRx":4, "garpTx":1, "transitions":2 }, "addresses":[ "192.0.2.1" ] }, "v6":{ "interface":"", "vmac":"00:00:5e:00:02:02", "primaryAddress":"::", "status":"Initialize", "effectivePriority":200, "masterAdverInterval":0, "skewTime":0, "masterDownInterval":0, "stats":{ "adverTx":0, "adverRx":0, "neighborAdverTx":0, "transitions":0 }, "addresses":[] } } ] prometheus-frr-exporter-1.11.0/collector/vrrp.go000066400000000000000000000101431516444222400217330ustar00rootroot00000000000000package collector import ( "encoding/json" "log/slog" "strconv" "strings" "github.com/prometheus/client_golang/prometheus" ) const ( vrrpStatusInitialize = "Initialize" vrrpStatusBackup = "Backup" vrrpStatusMaster = "Master" ) var ( vrrpSubsystem = "vrrp" vrrpStates = []string{vrrpStatusInitialize, vrrpStatusMaster, vrrpStatusBackup} ) func init() { registerCollector(vrrpSubsystem, disabledByDefault, NewVRRPCollector) } type VrrpVrInfo struct { Vrid uint32 Interface string V6Info VrrpInstanceInfo `json:"v6"` V4Info VrrpInstanceInfo `json:"v4"` } type VrrpInstanceInfo struct { Subinterface string `json:"interface"` Status string Statistics VrrpInstanceStats `json:"stats"` } type VrrpInstanceStats struct { AdverTx *uint32 AdverRx *uint32 GarpTx *uint32 NeighborAdverTx *uint32 Transitions *uint32 } type vrrpCollector struct { logger *slog.Logger descriptions map[string]*prometheus.Desc } // NewVRRPCollector collects VRRP metrics, implemented as per the Collector interface. func NewVRRPCollector(logger *slog.Logger) (Collector, error) { return &vrrpCollector{logger: logger, descriptions: getVRRPDesc()}, nil } func getVRRPDesc() map[string]*prometheus.Desc { labels := []string{"proto", "vrid", "interface", "subinterface"} stateLabels := append(labels, "state") return map[string]*prometheus.Desc{ "vrrpState": colPromDesc(vrrpSubsystem, "state", "Status of the VRRP state machine.", stateLabels), "adverTx": colPromDesc(vrrpSubsystem, "advertisements_sent_total", "Advertisements sent total.", labels), "adverRx": colPromDesc(vrrpSubsystem, "advertisements_received_total", "Advertisements received total.", labels), "garpTx": colPromDesc(vrrpSubsystem, "gratuitous_arp_sent_total", "Gratuitous ARP sent total.", labels), "neighborAdverTx": colPromDesc(vrrpSubsystem, "neighbor_advertisements_sent_total", "Neighbor Advertisements sent total.", labels), "transitions": colPromDesc(vrrpSubsystem, "state_transitions_total", "Number of transitions of the VRRP state machine in total.", labels), } } // Update implemented as per the Collector interface. func (c *vrrpCollector) Update(ch chan<- prometheus.Metric) error { cmd := "show vrrp json" jsonVRRPInfo, err := executeVRRPCommand(cmd) if err != nil { return err } if err := processVRRPInfo(ch, jsonVRRPInfo, c.descriptions); err != nil { return cmdOutputProcessError(cmd, string(jsonVRRPInfo), err) } return nil } func processVRRPInfo(ch chan<- prometheus.Metric, jsonVRRPInfo []byte, desc map[string]*prometheus.Desc) error { var jsonList []VrrpVrInfo if err := json.Unmarshal(jsonVRRPInfo, &jsonList); err != nil { return err } for _, vrInfo := range jsonList { processInstance(ch, "v4", vrInfo.Vrid, vrInfo.Interface, vrInfo.V4Info, desc) processInstance(ch, "v6", vrInfo.Vrid, vrInfo.Interface, vrInfo.V6Info, desc) } return nil } func processInstance(ch chan<- prometheus.Metric, proto string, vrid uint32, iface string, instance VrrpInstanceInfo, vrrpDesc map[string]*prometheus.Desc) { vrrpLabels := []string{proto, strconv.FormatUint(uint64(vrid), 10), iface, instance.Subinterface} for _, state := range vrrpStates { stateLabels := append(vrrpLabels, state) var value float64 if strings.EqualFold(instance.Status, state) { value = 1 } newGauge(ch, vrrpDesc["vrrpState"], value, stateLabels...) } if instance.Statistics.AdverTx != nil { newCounter(ch, vrrpDesc["adverTx"], float64(*instance.Statistics.AdverTx), vrrpLabels...) } if instance.Statistics.AdverRx != nil { newCounter(ch, vrrpDesc["adverRx"], float64(*instance.Statistics.AdverRx), vrrpLabels...) } if instance.Statistics.GarpTx != nil { newCounter(ch, vrrpDesc["garpTx"], float64(*instance.Statistics.GarpTx), vrrpLabels...) } if instance.Statistics.NeighborAdverTx != nil { newCounter(ch, vrrpDesc["neighborAdverTx"], float64(*instance.Statistics.NeighborAdverTx), vrrpLabels...) } if instance.Statistics.Transitions != nil { newCounter(ch, vrrpDesc["transitions"], float64(*instance.Statistics.Transitions), vrrpLabels...) } } prometheus-frr-exporter-1.11.0/collector/vrrp_test.go000066400000000000000000000120311516444222400227700ustar00rootroot00000000000000package collector import ( "fmt" "regexp" "strings" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) var expectedVRRPMetrics = map[string]float64{ "frr_vrrp_advertisements_received_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_1,vrid=1}": 1548196, "frr_vrrp_advertisements_received_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_2,vrid=2}": 4.0, "frr_vrrp_advertisements_received_total{interface=gw_extnet,proto=v6,subinterface=,vrid=2}": 0.0, "frr_vrrp_advertisements_received_total{interface=gw_extnet,proto=v6,subinterface=extnet_v6_1,vrid=1}": 1548195, "frr_vrrp_advertisements_sent_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_1,vrid=1}": 6, "frr_vrrp_advertisements_sent_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_2,vrid=2}": 1548210, "frr_vrrp_advertisements_sent_total{interface=gw_extnet,proto=v6,subinterface=,vrid=2}": 0, "frr_vrrp_advertisements_sent_total{interface=gw_extnet,proto=v6,subinterface=extnet_v6_1,vrid=1}": 2, "frr_vrrp_gratuitous_arp_sent_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_1,vrid=1}": 4, "frr_vrrp_gratuitous_arp_sent_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_2,vrid=2}": 1, "frr_vrrp_neighbor_advertisements_sent_total{interface=gw_extnet,proto=v6,subinterface=,vrid=2}": 0, "frr_vrrp_neighbor_advertisements_sent_total{interface=gw_extnet,proto=v6,subinterface=extnet_v6_1,vrid=1}": 5, "frr_vrrp_state_transitions_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_1,vrid=1}": 9, "frr_vrrp_state_transitions_total{interface=gw_extnet,proto=v4,subinterface=extnet_v4_2,vrid=2}": 2, "frr_vrrp_state_transitions_total{interface=gw_extnet,proto=v6,subinterface=,vrid=2}": 0, "frr_vrrp_state_transitions_total{interface=gw_extnet,proto=v6,subinterface=extnet_v6_1,vrid=1}": 11, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Backup,subinterface=extnet_v4_1,vrid=1}": 1, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Backup,subinterface=extnet_v4_2,vrid=2}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Initialize,subinterface=extnet_v4_1,vrid=1}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Initialize,subinterface=extnet_v4_2,vrid=2}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Master,subinterface=extnet_v4_1,vrid=1}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v4,state=Master,subinterface=extnet_v4_2,vrid=2}": 1, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Backup,subinterface=,vrid=2}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Backup,subinterface=extnet_v6_1,vrid=1}": 1, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Initialize,subinterface=,vrid=2}": 1, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Initialize,subinterface=extnet_v6_1,vrid=1}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Master,subinterface=,vrid=2}": 0, "frr_vrrp_state{interface=gw_extnet,proto=v6,state=Master,subinterface=extnet_v6_1,vrid=1}": 0, } func TestProcessVRRPInfo(t *testing.T) { ch := make(chan prometheus.Metric, 1024) if err := processVRRPInfo(ch, readTestFixture(t, "show_vrrp.json"), getVRRPDesc()); err != nil { t.Errorf("error calling processVRRPInfo: %s", err) } close(ch) // Create a map of following format: // key: metric_name{labelname:labelvalue,...} // value: metric value gotMetrics := make(map[string]float64) for { msg, more := <-ch if !more { break } metric := &dto.Metric{} if err := msg.Write(metric); err != nil { t.Errorf("error writing metric: %s", err) } var labels []string for _, label := range metric.GetLabel() { labels = append(labels, fmt.Sprintf("%s=%s", label.GetName(), label.GetValue())) } var value float64 if metric.GetCounter() != nil { value = metric.GetCounter().GetValue() } else if metric.GetGauge() != nil { value = metric.GetGauge().GetValue() } re, err := regexp.Compile(`.*fqName: "(.*)", help:.*`) if err != nil { t.Errorf("could not compile regex: %s", err) } metricName := re.FindStringSubmatch(msg.Desc().String())[1] gotMetrics[fmt.Sprintf("%s{%s}", metricName, strings.Join(labels, ","))] = value } for metricName, metricVal := range gotMetrics { if expectedMetricVal, ok := expectedVRRPMetrics[metricName]; ok { if expectedMetricVal != metricVal { t.Errorf("metric %s expected value %v got %v", metricName, expectedMetricVal, metricVal) } } else { t.Errorf("unexpected metric: %s : %v", metricName, metricVal) } } for expectedMetricName, expectedMetricVal := range expectedVRRPMetrics { if _, ok := gotMetrics[expectedMetricName]; !ok { t.Errorf("missing metric: %s value %v", expectedMetricName, expectedMetricVal) } } } prometheus-frr-exporter-1.11.0/dashboards/000077500000000000000000000000001516444222400205405ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/dashboards/grafana-bgp.json000066400000000000000000000705201516444222400236040ustar00rootroot00000000000000{ "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "11.5.2" }, { "type": "panel", "id": "heatmap", "name": "Heatmap", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "state-timeline", "name": "State timeline", "version": "" }, { "type": "panel", "id": "table", "name": "Table", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "BGP metrics from https://github.com/tynany/frr_exporter", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "links": [], "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "fillOpacity": 69, "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineWidth": 0, "spanNulls": false }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 }, { "color": "#EAB839", "value": 2 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 20, "x": 0, "y": 0 }, "id": 1, "options": { "alignValue": "left", "legend": { "displayMode": "list", "placement": "bottom", "showLegend": false }, "mergeValues": true, "rowHeight": 0.85, "showValue": "never", "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_peer_state{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "{{peer_desc}}", "range": true, "refId": "A" } ], "title": "Peer State History", "transparent": true, "type": "state-timeline" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 0 }, "id": 5, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_peers_count_total{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Peers", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 3 }, "id": 6, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_peer_groups_count_total{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Peer Groups", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 6 }, "id": 7, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_rib_memory_bytes{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Routing Table Size", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "left", "cellOptions": { "type": "auto" }, "filterable": false, "inspect": false }, "fieldMinMax": true, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "Status" }, "properties": [ { "id": "mappings", "value": [ { "options": { "0": { "index": 0, "text": "DOWN" }, "1": { "index": 1, "text": "UP" }, "2": { "index": 2, "text": "SHUTDOWN" } }, "type": "value" } ] }, { "id": "custom.cellOptions", "value": { "applyToRow": false, "type": "color-background" } }, { "id": "custom.width", "value": 98 }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 }, { "color": "#EAB839", "value": 2 } ] } } ] }, { "matcher": { "id": "byName", "options": "Description" }, "properties": [ { "id": "custom.width", "value": 180 } ] }, { "matcher": { "id": "byName", "options": "Peer" }, "properties": [ { "id": "custom.width", "value": 185 } ] }, { "matcher": { "id": "byName", "options": "Prefixes Received" }, "properties": [ { "id": "custom.cellOptions", "value": { "mode": "basic", "type": "gauge", "valueDisplayMode": "text" } }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } } ] }, { "matcher": { "id": "byName", "options": "Peer IP" }, "properties": [ { "id": "custom.width", "value": 177 } ] }, { "matcher": { "id": "byName", "options": "Peer AS" }, "properties": [ { "id": "custom.width", "value": 120 } ] }, { "matcher": { "id": "byName", "options": "Prefixes Advertised" }, "properties": [ { "id": "custom.cellOptions", "value": { "mode": "basic", "type": "gauge" } } ] }, { "matcher": { "id": "byName", "options": "State Duration" }, "properties": [ { "id": "unit", "value": "dtdhms" }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "#ea495c", "value": null }, { "color": "#EAB839", "value": 900 }, { "color": "green", "value": 3600 } ] } }, { "id": "custom.cellOptions", "value": { "type": "color-text" } }, { "id": "custom.width", "value": 119 } ] } ] }, "gridPos": { "h": 10, "w": 20, "x": 0, "y": 8 }, "id": 2, "options": { "cellHeight": "sm", "footer": { "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [] }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "frr_bgp_peer_state{afi=\"$afi\",instance=\"$instance\"}", "format": "table", "instant": true, "legendFormat": "State", "range": false, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "frr_bgp_peer_prefixes_received_count_total{afi=\"$afi\",instance=\"$instance\"}", "format": "table", "hide": false, "instant": true, "legendFormat": "{{label_name}}", "range": false, "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "frr_bgp_peer_prefixes_advertised_count_total{afi=\"$afi\",instance=\"$instance\"}", "format": "table", "hide": false, "instant": true, "legendFormat": "__auto", "range": false, "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": false, "expr": "frr_bgp_peer_uptime_seconds{afi=\"$afi\",instance=\"$instance\"}", "format": "table", "hide": false, "instant": true, "legendFormat": "__auto", "range": false, "refId": "D" } ], "title": "Session Status", "transformations": [ { "id": "joinByField", "options": { "byField": "peer", "mode": "outer" } }, { "id": "organize", "options": { "excludeByName": { "Time 1": true, "Time 2": true, "Time 3": true, "Time 4": true, "__name__ 1": true, "__name__ 2": true, "__name__ 3": true, "__name__ 4": true, "afi 1": true, "afi 2": true, "afi 3": true, "afi 4": true, "instance 1": true, "instance 2": true, "instance 3": true, "instance 4": true, "job 1": true, "job 2": true, "job 3": true, "job 4": true, "local_as 1": true, "local_as 2": true, "local_as 3": true, "local_as 4": true, "peer_as 2": true, "peer_as 3": true, "peer_as 4": true, "peer_desc 2": true, "peer_desc 3": true, "peer_desc 4": true, "safi 1": true, "safi 2": true, "safi 3": true, "safi 4": true, "vrf 1": true, "vrf 2": true, "vrf 3": true, "vrf 4": true }, "includeByName": {}, "indexByName": { "Time 1": 4, "Time 2": 13, "Time 3": 24, "Time 4": 35, "Value #A": 1, "Value #B": 23, "Value #C": 34, "Value #D": 2, "__name__ 1": 5, "__name__ 2": 14, "__name__ 3": 25, "__name__ 4": 36, "afi 1": 6, "afi 2": 15, "afi 3": 26, "afi 4": 37, "instance 1": 7, "instance 2": 16, "instance 3": 27, "instance 4": 38, "job 1": 8, "job 2": 17, "job 3": 28, "job 4": 39, "local_as 1": 9, "local_as 2": 18, "local_as 3": 29, "local_as 4": 40, "peer": 3, "peer_as 1": 10, "peer_as 2": 19, "peer_as 3": 30, "peer_as 4": 41, "peer_desc 1": 0, "peer_desc 2": 20, "peer_desc 3": 31, "peer_desc 4": 42, "safi 1": 11, "safi 2": 21, "safi 3": 32, "safi 4": 43, "vrf 1": 12, "vrf 2": 22, "vrf 3": 33, "vrf 4": 44 }, "renameByName": { "Value #A": "Status", "Value #B": "Prefixes Received", "Value #C": "Prefixes Advertised", "Value #D": "State Duration", "peer": "Peer IP", "peer_as 1": "Peer AS", "peer_desc 1": "Description", "vrf 1": "" } } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 10 }, "id": 8, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_peers_memory_bytes{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Peer Memory Usage", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "fixed" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 14 }, "id": 9, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "frr_bgp_rib_count_total{afi=\"$afi\",instance=\"$instance\"}", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Total Routes", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "custom": { "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "scaleDistribution": { "type": "linear" } } }, "overrides": [] }, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 18 }, "id": 3, "options": { "calculate": false, "cellGap": 1, "color": { "exponent": 0.5, "fill": "blue", "mode": "scheme", "reverse": false, "scale": "exponential", "scheme": "Turbo", "steps": 77 }, "exemplars": { "color": "rgba(255,0,255,0.7)" }, "filterValues": { "le": 1e-9 }, "legend": { "show": true }, "rowsFrame": { "layout": "auto" }, "tooltip": { "mode": "single", "showColorScale": false, "yHistogram": false }, "yAxis": { "axisPlacement": "left", "reverse": false } }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(frr_bgp_peer_message_received_total{afi=\"$afi\",instance=\"$instance\"}[$__rate_interval])", "instant": false, "legendFormat": "{{peer_desc}}", "range": true, "refId": "A" } ], "title": "Peer Update Rate", "transparent": true, "type": "heatmap" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": true, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 44, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineStyle": { "fill": "solid" }, "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 9, "w": 24, "x": 0, "y": 26 }, "id": 10, "options": { "legend": { "calcs": [ "min", "max", "mean" ], "displayMode": "table", "placement": "right", "showLegend": true, "sortBy": "Name", "sortDesc": true }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "pluginVersion": "11.5.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(frr_bgp_peer_message_received_total{afi=\"$afi\",instance=\"$instance\"}[$__rate_interval])", "instant": false, "legendFormat": "{{peer_desc}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "-irate(frr_bgp_peer_message_sent_total{afi=\"ipv4\",instance=\"$instance\"}[$__rate_interval])", "hide": false, "instant": false, "legendFormat": "{{peer_desc}}", "range": true, "refId": "B" } ], "title": "BGP Messages", "type": "timeseries" } ], "refresh": "1m", "schemaVersion": 40, "tags": [ "frr", "bgp" ], "templating": { "list": [ { "allowCustomValue": false, "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(frr_bgp_peer_state,afi)", "description": "Address family", "label": "Protocol", "name": "afi", "options": [], "query": { "qryType": 1, "query": "label_values(frr_bgp_peer_state,afi)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(frr_bgp_peer_state,instance)", "description": "Exporter instance", "label": "Instance", "name": "instance", "options": [], "query": { "qryType": 1, "query": "label_values(frr_bgp_peer_state,instance)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", "type": "query" } ] }, "time": { "from": "now-3h", "to": "now" }, "timepicker": {}, "timezone": "browser", "title": "FRR Exporter - BGP", "uid": "deecmekf9dbeoa", "version": 10, "weekStart": "" }prometheus-frr-exporter-1.11.0/dev/000077500000000000000000000000001516444222400172045ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/dev/Makefile000066400000000000000000000266631516444222400206610ustar00rootroot00000000000000# FRR Exporter Development Environment # ===================================== SHELL := /bin/bash COMPOSE := docker compose FRR_VERSION ?= 10.5.1 TEST_VERSIONS ?= 7.5.1 8.5.7 10.5.1 # Exporter binary location (use /tmp for Lima VM compatibility) EXPORTER_BIN := /tmp/frr_exporter_linux .PHONY: help up down build deploy metrics metrics2 vtysh1 vtysh2 status bgp ospf bfd pim vrrp routes test-all-versions clean \ lima-setup lima-provision lima-shell lima-start lima-stop lima-delete help: @echo "FRR Exporter Development Environment" @echo "====================================" @echo "" @echo "Quick Start:" @echo " make up - Start FRR routers (FRR $(FRR_VERSION))" @echo " make build - Build frr_exporter" @echo " make deploy - Deploy exporter to containers" @echo " make redeploy - Rebuild and redeploy exporter" @echo " make down - Stop environment" @echo " make restart - Restart environment" @echo " make clean - Stop and remove built binary" @echo "" @echo "Or all at once:" @echo " make up build deploy - Start everything" @echo "" @echo "Different FRR version:" @echo " make up FRR_VERSION=7.5.1" @echo "" @echo "Metrics:" @echo " make metrics1 - View metrics from router1" @echo " make metrics2 - View metrics from router2" @echo "" @echo "Debugging:" @echo " make vtysh1 - Open vtysh on router1" @echo " make vtysh2 - Open vtysh on router2" @echo " make status - Show all protocol status" @echo " make show-bgp - Show BGP status" @echo " make show-ospf - Show OSPF status" @echo " make show-bfd - Show BFD status" @echo " make show-pim - Show PIM status" @echo " make show-vrrp - Show VRRP status" @echo " make show-routes - Show route summary" @echo " make show-version - Show FRR version" @echo " make logs - View FRR container logs" @echo " make exporter-logs - View exporter logs on router1 and router2" @echo " make exporter-logs-follow-router1 - Tail exporter logs on router1" @echo " make exporter-logs-follow-router2 - Tail exporter logs on router2" @echo "" @echo "Testing different versions:" @echo " make test-all-versions TEST_VERSIONS=\"8.5.7 10.5.1\"" @echo "" @echo "macOS Setup (VRF requires Lima VM):" @echo " make lima-setup - Create Ubuntu VM" @echo " make lima-provision - Install Docker and tools in VM" @echo " make lima-shell - Shell into VM" @echo " make lima-start - Start the VM" @echo " make lima-stop - Stop the VM" @echo " make lima-delete - Remove the VM" @echo "" @echo "Endpoints (after deploy):" @echo " http://localhost:9342/metrics - Router1" @echo " http://localhost:9343/metrics - Router2" # Start FRR containers up: @echo "Starting FRR $(FRR_VERSION) routers:" FRR_VERSION=$(FRR_VERSION) $(COMPOSE) up -d @echo "Sleeping to ensure containers have started:" @sleep 5 @$(COMPOSE) ps @echo "" @echo "FRR ready:" @echo " Router1: make vtysh1" @echo " Router2: make vtysh2" @echo "Start exporter: make build deploy" # Stop all containers down: $(COMPOSE) down @echo "Stopped." # Restart restart: down up # Build frr_exporter build: @echo "Building frr_exporter for Linux:" cd .. && go build -o $(EXPORTER_BIN) . @echo "Built: $(EXPORTER_BIN)" # Deploy exporter to containers and start it deploy: $(EXPORTER_BIN) @echo "Deploying exporter to containers:" docker cp $(EXPORTER_BIN) frr-router1:/usr/local/bin/frr_exporter docker cp $(EXPORTER_BIN) frr-router2:/usr/local/bin/frr_exporter -docker exec frr-router1 pkill -f frr_exporter 2>/dev/null || true -docker exec frr-router2 pkill -f frr_exporter 2>/dev/null || true @sleep 1 docker exec -d frr-router1 sh -c '/usr/local/bin/frr_exporter \ --collector.bgp \ --collector.bgp6 \ --collector.ospf \ --collector.bfd \ --collector.pim \ --collector.vrrp \ --collector.route \ --collector.route.detailed-routes \ --collector.bgp.peer-descriptions \ --web.listen-address=:9342 \ 2>&1 | tee /var/log/frr_exporter.log' docker exec -d frr-router2 sh -c '/usr/local/bin/frr_exporter \ --collector.bgp \ --collector.bgp6 \ --collector.ospf \ --collector.bfd \ --collector.pim \ --collector.vrrp \ --collector.route \ --collector.route.detailed-routes \ --collector.bgp.peer-descriptions \ --web.listen-address=:9342 \ 2>&1 | tee /var/log/frr_exporter.log' @echo "" @echo "Exporters started:" @echo " Router1: http://localhost:9342/metrics" @echo " Router2: http://localhost:9343/metrics" @echo "" @echo "View logs: make exporter-logs" $(EXPORTER_BIN): @echo "Exporter binary not found. Building:" $(MAKE) build # Redeploy (rebuild and deploy) redeploy: build deploy # View FRR logs logs: $(COMPOSE) logs -f logs-router1: $(COMPOSE) logs -f router1 logs-router2: $(COMPOSE) logs -f router2 # View exporter logs exporter-logs: @echo "=== Router1 Exporter Logs ===" @docker exec frr-router1 cat /var/log/frr_exporter.log @echo "" @echo "=== Router2 Exporter Logs ===" @docker exec frr-router2 cat /var/log/frr_exporter.log exporter-logs-follow-router1: docker exec frr-router1 tail -f /var/log/frr_exporter.log exporter-logs-follow-router2: docker exec frr-router2 tail -f /var/log/frr_exporter.log # Fetch metrics from router1 metrics1: @echo "=== FRR Exporter Metrics (Router1) ===" @curl -s http://localhost:9342/metrics @echo "" # Fetch metrics from router2 metrics2: @echo "=== FRR Exporter Metrics (Router2) ===" @curl -s http://localhost:9343/metrics @echo "" # Open vtysh on router1 vtysh1: docker exec -it frr-router1 vtysh # Open vtysh on router2 vtysh2: docker exec -it frr-router2 vtysh # Show overall status status: show-bgp show-ospf show-bfd show-pim show-vrrp # Show BGP status show-bgp: @echo "=== BGP Status (Router1) ===" @docker exec frr-router1 vtysh -c "show bgp summary" @echo "" @echo "=== BGP VRF vrf-red Status ===" @docker exec frr-router1 vtysh -c "show bgp vrf vrf-red summary" # Show OSPF status show-ospf: @echo "=== OSPF Neighbors (Router1) ===" @docker exec frr-router1 vtysh -c "show ip ospf neighbor" @echo "=== OSPF Neighbors (Router2) ===" @docker exec frr-router2 vtysh -c "show ip ospf neighbor" # Show BFD status show-bfd: @echo "=== BFD Peers (Router1) ===" @docker exec frr-router1 vtysh -c "show bfd peers" @echo "=== BFD Peers (Route2) ===" @docker exec frr-router2 vtysh -c "show bfd peers" # Show PIM status show-pim: @echo "=== PIM Neighbors (Router1) ===" @docker exec frr-router1 vtysh -c "show ip pim neighbor" @echo "=== PIM Neighbors (Router2) ===" @docker exec frr-router2 vtysh -c "show ip pim neighbor" # Show VRRP status show-vrrp: @echo "=== VRRP Status (Router1) ===" @docker exec frr-router1 vtysh -c "show vrrp" 2>/dev/null # Show routes show-routes: @echo "=== IP Routes (Router1) ===" @docker exec frr-router1 vtysh -c "show ip route summary" @echo "=== IP Routes (Router2) ===" @docker exec frr-router2 vtysh -c "show ip route summary" # Show FRR version show-version: @echo "=== Version (Router1) ===" @docker exec frr-router1 vtysh -c "show version" @echo "=== Version (Router2) ===" @docker exec frr-router2 vtysh -c "show version" # Test all supported FRR versions test-all-versions: build @echo "Testing FRR Exporter with multiple FRR versions:" @echo "==================================================" @for version in $(TEST_VERSIONS); do \ echo ""; \ echo "=== Testing FRR $$version ==="; \ echo "----------------------------"; \ $(MAKE) down 2>/dev/null || true; \ $(MAKE) up FRR_VERSION=$$version; \ sleep 10; \ $(MAKE) deploy; \ sleep 3; \ echo ""; \ echo "Checking metrics on router1:"; \ curl -s http://localhost:9342/metrics echo "Checking metrics on router2:"; \ curl -s http://localhost:9343/metrics done @echo "" @echo "All version tests complete!" @$(MAKE) down # ============================================================================= # macOS Lima VM Setup (required for VRF support) # ============================================================================= # VRF requires CONFIG_NET_VRF kernel support, which is not available in Docker # Desktop or Colima. Lima with Ubuntu provides a full Linux kernel with VRF. LIMA_VM := frrdev # Check if running on macOS IS_MACOS := $(shell uname -s | grep -q Darwin && echo 1 || echo 0) # Create Lima VM (does not provision - use lima-provision for that) lima-setup: ifeq ($(IS_MACOS),0) @echo "Lima is only needed on macOS. On Linux, use Docker directly." else @if ! command -v limactl &> /dev/null; then \ echo "Error: Lima not found. Install with: brew install lima"; \ exit 1; \ fi @if limactl list 2>/dev/null | grep -q "$(LIMA_VM).*Running"; then \ echo "VM '$(LIMA_VM)' is already running."; \ elif limactl list 2>/dev/null | grep -q "$(LIMA_VM)"; then \ limactl start $(LIMA_VM); \ else \ echo "Creating Ubuntu VM '$(LIMA_VM)':"; \ limactl start --tty=false --name=$(LIMA_VM) template:ubuntu-24.04; \ echo ""; \ echo "VM created. Now run 'make lima-provision' to install Docker and tools."; \ fi endif # Install Docker, Go, and tools inside the Lima VM GO_VERSION := 1.25.0 lima-provision: @echo "Provisioning Lima VM with Docker, Go, and tools:" limactl shell $(LIMA_VM) -- sudo apt-get update limactl shell $(LIMA_VM) -- sudo apt-get install -y \ docker.io \ docker-compose-v2 \ make \ curl \ git @echo "Installing VRF kernel module:" -limactl shell $(LIMA_VM) -- bash -c "sudo apt-get install -y linux-modules-extra-\$$(uname -r)" -limactl shell $(LIMA_VM) -- sudo modprobe vrf echo "Starting Docker:" limactl shell $(LIMA_VM) -- sudo systemctl enable docker limactl shell $(LIMA_VM) -- sudo systemctl start docker limactl shell $(LIMA_VM) -- sh -c 'sudo usermod -aG docker "$$(whoami)"' @echo "Installing Go $(GO_VERSION):" limactl shell $(LIMA_VM) -- curl -fsSL https://go.dev/dl/go$(GO_VERSION).linux-arm64.tar.gz -o /tmp/go.tar.gz limactl shell $(LIMA_VM) -- sudo rm -rf /usr/local/go limactl shell $(LIMA_VM) -- sudo tar -C /usr/local -xzf /tmp/go.tar.gz limactl shell $(LIMA_VM) -- rm /tmp/go.tar.gz limactl shell $(LIMA_VM) -- bash -c "echo 'export PATH=\$$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/golang.sh >/dev/null" limactl shell $(LIMA_VM) -- sudo systemctl start docker.service @echo "Rebooting to apply settings:" @limactl stop $(LIMA_VM) @limactl start $(LIMA_VM) @echo "" @echo "========================================" @echo "Lima VM provisioned:" @echo "" @echo "Run: make lima-shell" @echo "Then: make up build deploy" @echo "========================================" # Shell into Lima VM lima-shell: @limactl shell $(LIMA_VM) # Start Lima VM lima-start: @limactl start $(LIMA_VM) # Stop Lima VM lima-stop: @limactl stop $(LIMA_VM) # Delete Lima VM lima-delete: @limactl delete $(LIMA_VM) # ============================================================================= # Cleanup # ============================================================================= # Clean up clean: down rm -f $(EXPORTER_BIN) @echo "Cleaned." prometheus-frr-exporter-1.11.0/dev/README.md000066400000000000000000000137271516444222400204750ustar00rootroot00000000000000# FRR Exporter Development Environment Docker-based environment for testing frr_exporter against real FRR instances. Also includes setting up a Ubuntu VM using Lima on MacOS for VRF support. ## Quick Start ```bash # Start FRR routers, build exporter, and deploy make up build deploy # View metrics make metrics1 make metrics2 # Check protocol status make status # Stop make down ``` ### MacOS ```bash # Create Ubuntu VM make lima-setup # Install Docker, make, and tools make lima-provision # Shell into the VM (your mac filesystem is mounted) make lima-shell ``` ## Testing Different FRR Versions ```bash make up FRR_VERSION=7.5.1 && make deploy make up FRR_VERSION=8.5.7 && make deploy make up FRR_VERSION=10.5.1 && make deploy # default # Test specific versions make test-all-versions TEST_VERSIONS="8.5.7 10.5.1" ``` ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ┌──────────────┐ eth0 (10.0.0.0/24) ┌──────────────┐ │ │ │ Router1 │◄───────────────────────►│ Router2 │ │ │ │ AS 65001 │ BGP/OSPF/BFD/PIM │ AS 65002 │ │ │ │ 10.0.0.10 │ VRRP │ 10.0.0.11 │ │ │ │ :9342 │ │ :9343 │ │ │ │ │◄───────────────────────►│ │ │ │ │ 10.1.0.10 │ eth1 VRF (10.1.0.0/24) │ 10.1.0.11 │ │ │ └──────────────┘ BGP/OSPF/BFD └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Protocol Coverage | Protocol | Default VRF (eth0) | VRF vrf-red (eth1) | |----------|--------------------|--------------------| | BGP | 1 peer | 1 peer | | OSPF | Area 0 | Area 0 | | BFD | 1 peer | 1 peer | | PIM | Neighbor | - | | VRRP | VRID 10 | - | | Routes | Static | - | ## Requirements VRF support requires a Linux kernel with `CONFIG_NET_VRF`. This module is not available in Docker Desktop or Colima's lightweight VMs. ### Linux Works out of the box as long as Docker is installed. ### macOS (Intel & Apple Silicon) You need a full Ubuntu VM. The Makefile automates this via Lima: ```bash # One-time setup make lima-setup # Create Ubuntu VM make lima-provision # Install Docker, make, and tools # Shell into the VM (your mac filesystem is mounted) make lima-shell # Inside the VM: run the dev environment make up build deploy make metrics ``` **Lima VM management:** ```bash make lima-stop # Stop the VM make lima-start # Start it again make lima-delete # Remove completely ``` ## Make Targets ``` FRR Exporter Development Environment ==================================== Quick Start: make up - Start FRR routers (FRR 10.5.1) make build - Build frr_exporter make deploy - Deploy exporter to containers make redeploy - Rebuild and redeploy exporter make down - Stop environment make restart - Restart environment make clean - Stop and remove built binary Or all at once: make up build deploy - Start everything Different FRR version: make up FRR_VERSION=7.5.1 Metrics: make metrics1 - View metrics from router1 make metrics2 - View metrics from router2 Debugging: make vtysh1 - Open vtysh on router1 make vtysh2 - Open vtysh on router2 make status - Show all protocol status make show-bgp - Show BGP status make show-ospf - Show OSPF status make show-bfd - Show BFD status make show-pim - Show PIM status make show-vrrp - Show VRRP status make show-routes - Show route summary make show-version - Show FRR version make logs - View FRR container logs make exporter-logs - View exporter logs on router1 and router2 make exporter-logs-follow-router1 - Tail exporter logs on router1 make exporter-logs-follow-router2 - Tail exporter logs on router2 Testing different versions: make test-all-versions TEST_VERSIONS="8.5.7 10.5.1" macOS Setup (VRF requires Lima VM): make lima-setup - Create Ubuntu VM make lima-provision - Install Docker and tools in VM make lima-shell - Shell into VM make lima-start - Start the VM make lima-stop - Stop the VM make lima-delete - Remove the VM Endpoints (after deploy): http://localhost:9342/metrics - Router1 http://localhost:9343/metrics - Router2 ``` ## Development Workflow After making code changes: ```bash make redeploy # Rebuilds and restarts exporter make metrics1 make metrics2 ``` ## Endpoints | URL | Description | |-----|-------------| | http://localhost:9342/metrics | Router1 exporter | | http://localhost:9343/metrics | Router2 exporter | prometheus-frr-exporter-1.11.0/dev/configs/000077500000000000000000000000001516444222400206345ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/dev/configs/daemons000066400000000000000000000010041516444222400222000ustar00rootroot00000000000000# FRR daemons configuration for frr_exporter testing # Enable all daemons needed by collectors zebrad=yes bgpd=yes ospfd=yes ospf6d=no ripd=no ripngd=no isisd=no pimd=yes pim6d=no ldpd=no nhrpd=no eigrpd=no babeld=no sharpd=no pbrd=no bfdd=yes fabricd=no vrrpd=yes pathd=no # Integrated config mode vtysh_enable=yes zebra_options=" -A 127.0.0.1 -s 90000000" bgpd_options=" -A 127.0.0.1" ospfd_options=" -A 127.0.0.1" pimd_options=" -A 127.0.0.1" bfdd_options=" -A 127.0.0.1" vrrpd_options=" -A 127.0.0.1" prometheus-frr-exporter-1.11.0/dev/configs/router1.conf000066400000000000000000000065131516444222400231110ustar00rootroot00000000000000! FRR Configuration for Router1 - frr_exporter testing ! Compatible with FRR 7.5+ through 10.x frr defaults traditional hostname router1 log syslog informational service integrated-vtysh-config ! ============================================ ! VRF Configuration ! ============================================ vrf vrf-red exit-vrf ! ============================================ ! Interface Configuration - Default VRF ! ============================================ interface eth0 description Backbone link to router2 ip address 10.0.0.10/24 ip ospf area 0 ip ospf network broadcast ip pim sm exit interface lo ip address 1.1.1.1/32 ip ospf area 0 exit ! ============================================ ! Interface Configuration - VRF RED ! ============================================ interface eth1 vrf vrf-red description VRF RED link to router2 ip address 10.1.0.10/24 ip ospf area 0 exit ! ============================================ ! BGP Configuration - Default VRF ! ============================================ router bgp 65001 bgp router-id 1.1.1.1 neighbor 10.0.0.11 remote-as 65002 neighbor 10.0.0.11 description {"type":"spine","role":"peer"} neighbor 10.0.0.11 bfd ! address-family ipv4 unicast network 1.1.1.1/32 network 10.0.0.0/24 neighbor 10.0.0.11 activate neighbor 10.0.0.11 route-map PERMIT-ALL in neighbor 10.0.0.11 route-map PERMIT-ALL out exit-address-family exit ! ============================================ ! BGP Configuration - VRF RED ! ============================================ router bgp 65001 vrf vrf-red bgp router-id 1.1.1.1 neighbor 10.1.0.11 remote-as 65002 neighbor 10.1.0.11 description {"type":"leaf","role":"vrf-peer"} neighbor 10.1.0.11 bfd ! address-family ipv4 unicast network 10.1.0.0/24 neighbor 10.1.0.11 activate neighbor 10.1.0.11 route-map PERMIT-ALL in neighbor 10.1.0.11 route-map PERMIT-ALL out exit-address-family exit ! ============================================ ! OSPF Configuration - Default VRF ! ============================================ router ospf ospf router-id 1.1.1.1 redistribute connected redistribute bgp network 10.0.0.0/24 area 0 network 1.1.1.1/32 area 0 exit ! ============================================ ! OSPF Configuration - VRF RED ! ============================================ router ospf vrf vrf-red ospf router-id 1.1.1.1 redistribute connected network 10.1.0.0/24 area 0 exit ! ============================================ ! BFD Configuration ! ============================================ bfd peer 10.0.0.11 receive-interval 300 transmit-interval 300 exit peer 10.1.0.11 vrf vrf-red receive-interval 300 transmit-interval 300 exit exit ! ============================================ ! PIM Configuration ! ============================================ ip pim rp 1.1.1.1 ! ============================================ ! VRRP Configuration (FRR 7.2+) ! ============================================ interface eth0 vrrp 10 version 3 vrrp 10 priority 150 vrrp 10 ip 10.0.0.100 exit ! ============================================ ! Route-map for BGP ! ============================================ route-map PERMIT-ALL permit 10 exit ! ============================================ ! Static Routes for testing route collector ! ============================================ ip route 192.168.100.0/24 10.0.0.11 ip route 192.168.101.0/24 10.0.0.11 end prometheus-frr-exporter-1.11.0/dev/configs/router2.conf000066400000000000000000000065131516444222400231120ustar00rootroot00000000000000! FRR Configuration for Router2 - frr_exporter testing ! Compatible with FRR 7.5+ through 10.x frr defaults traditional hostname router2 log syslog informational service integrated-vtysh-config ! ============================================ ! VRF Configuration ! ============================================ vrf vrf-red exit-vrf ! ============================================ ! Interface Configuration - Default VRF ! ============================================ interface eth0 description Backbone link to router1 ip address 10.0.0.11/24 ip ospf area 0 ip ospf network broadcast ip pim sm exit interface lo ip address 2.2.2.2/32 ip ospf area 0 exit ! ============================================ ! Interface Configuration - VRF RED ! ============================================ interface eth1 vrf vrf-red description VRF RED link to router1 ip address 10.1.0.11/24 ip ospf area 0 exit ! ============================================ ! BGP Configuration - Default VRF ! ============================================ router bgp 65002 bgp router-id 2.2.2.2 neighbor 10.0.0.10 remote-as 65001 neighbor 10.0.0.10 description {"type":"spine","role":"peer"} neighbor 10.0.0.10 bfd ! address-family ipv4 unicast network 2.2.2.2/32 network 10.0.0.0/24 neighbor 10.0.0.10 activate neighbor 10.0.0.10 route-map PERMIT-ALL in neighbor 10.0.0.10 route-map PERMIT-ALL out exit-address-family exit ! ============================================ ! BGP Configuration - VRF RED ! ============================================ router bgp 65002 vrf vrf-red bgp router-id 2.2.2.2 neighbor 10.1.0.10 remote-as 65001 neighbor 10.1.0.10 description {"type":"leaf","role":"vrf-peer"} neighbor 10.1.0.10 bfd ! address-family ipv4 unicast network 10.1.0.0/24 neighbor 10.1.0.10 activate neighbor 10.1.0.10 route-map PERMIT-ALL in neighbor 10.1.0.10 route-map PERMIT-ALL out exit-address-family exit ! ============================================ ! OSPF Configuration - Default VRF ! ============================================ router ospf ospf router-id 2.2.2.2 redistribute connected redistribute bgp network 10.0.0.0/24 area 0 network 2.2.2.2/32 area 0 exit ! ============================================ ! OSPF Configuration - VRF RED ! ============================================ router ospf vrf vrf-red ospf router-id 2.2.2.2 redistribute connected network 10.1.0.0/24 area 0 exit ! ============================================ ! BFD Configuration ! ============================================ bfd peer 10.0.0.10 receive-interval 300 transmit-interval 300 exit peer 10.1.0.10 vrf vrf-red receive-interval 300 transmit-interval 300 exit exit ! ============================================ ! PIM Configuration ! ============================================ ip pim rp 1.1.1.1 ! ============================================ ! VRRP Configuration (FRR 7.2+) ! ============================================ interface eth0 vrrp 10 version 3 vrrp 10 priority 100 vrrp 10 ip 10.0.0.100 exit ! ============================================ ! Route-map for BGP ! ============================================ route-map PERMIT-ALL permit 10 exit ! ============================================ ! Static Routes for testing route collector ! ============================================ ip route 192.168.200.0/24 10.0.0.10 ip route 192.168.201.0/24 10.0.0.10 end prometheus-frr-exporter-1.11.0/dev/configs/startup.sh000077500000000000000000000011721516444222400226760ustar00rootroot00000000000000#!/bin/bash # Startup script for FRR containers # Creates VRF and configures interfaces before FRR starts set -e # Enable IP forwarding sysctl -w net.ipv4.conf.all.forwarding=1 &>/dev/null || true # Create VRF if kernel supports it if ip link add vrf-red type vrf table 10 2>/dev/null; then echo "Created VRF vrf-red" ip link set vrf-red up # Move eth1 into VRF if it exists if ip link show eth1 &>/dev/null; then ip link set eth1 master vrf-red echo "Moved eth1 to VRF vrf-red" fi else echo "VRF not supported by kernel - skipping VRF setup" fi # Start FRR exec /usr/lib/frr/docker-start prometheus-frr-exporter-1.11.0/dev/docker-compose.yml000066400000000000000000000033131516444222400226410ustar00rootroot00000000000000services: router1: image: quay.io/frrouting/frr:${FRR_VERSION:-10.5.1} container_name: frr-router1 hostname: router1 privileged: true sysctls: - net.ipv4.ip_forward=1 - net.ipv6.conf.all.forwarding=1 - net.ipv4.conf.all.rp_filter=0 cap_add: - NET_ADMIN - SYS_ADMIN volumes: - ./configs/router1.conf:/etc/frr/frr.conf:ro - ./configs/daemons:/etc/frr/daemons:ro - ./configs/startup.sh:/startup.sh:ro entrypoint: ["/bin/bash", "/startup.sh"] ports: - "9342:9342" networks: backbone: ipv4_address: 10.0.0.10 vrf-red: ipv4_address: 10.1.0.10 healthcheck: test: ["CMD", "vtysh", "-c", "show version"] interval: 5s timeout: 3s retries: 10 router2: image: quay.io/frrouting/frr:${FRR_VERSION:-10.5.1} container_name: frr-router2 hostname: router2 privileged: true sysctls: - net.ipv4.ip_forward=1 - net.ipv6.conf.all.forwarding=1 - net.ipv4.conf.all.rp_filter=0 cap_add: - NET_ADMIN - SYS_ADMIN volumes: - ./configs/router2.conf:/etc/frr/frr.conf:ro - ./configs/daemons:/etc/frr/daemons:ro - ./configs/startup.sh:/startup.sh:ro entrypoint: ["/bin/bash", "/startup.sh"] ports: - "9343:9342" networks: backbone: ipv4_address: 10.0.0.11 vrf-red: ipv4_address: 10.1.0.11 healthcheck: test: ["CMD", "vtysh", "-c", "show version"] interval: 5s timeout: 3s retries: 10 networks: backbone: driver: bridge ipam: config: - subnet: 10.0.0.0/24 vrf-red: driver: bridge ipam: config: - subnet: 10.1.0.0/24 prometheus-frr-exporter-1.11.0/frr_exporter.go000066400000000000000000000037621516444222400215060ustar00rootroot00000000000000package main import ( "fmt" "net/http" _ "net/http/pprof" "os" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog/flag" "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" "github.com/prometheus/exporter-toolkit/web/kingpinflag" "github.com/tynany/frr_exporter/collector" ) var ( telemetryPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() webFlagConfig = kingpinflag.AddFlags(kingpin.CommandLine, ":9342") ) func main() { promslogConfig := &promslog.Config{} flag.AddFlags(kingpin.CommandLine, promslogConfig) kingpin.Version(version.Print("frr_exporter")) kingpin.HelpFlag.Short('h') kingpin.Parse() logger := promslog.New(promslogConfig) prometheus.MustRegister(versioncollector.NewCollector("frr_exporter")) logger.Info("Starting frr_exporter", "version", version.Info()) logger.Info("Build context", "build_context", version.BuildContext()) nc, err := collector.NewExporter(logger) if err != nil { panic(fmt.Errorf("could not create collector: %w", err)) } prometheus.MustRegister(nc) http.Handle(*telemetryPath, promhttp.Handler()) if *telemetryPath != "/" && *telemetryPath != "" { landingConfig := web.LandingConfig{ Name: "FRR Exporter", Description: "Prometheus Exporter for FRRouting daemon", Version: version.Info(), Links: []web.LandingLinks{ {Address: *telemetryPath, Text: "Metrics"}, }, } landingPage, err := web.NewLandingPage(landingConfig) if err != nil { logger.Error(err.Error()) os.Exit(1) } http.Handle("/", landingPage) } server := &http.Server{} if err := web.ListenAndServe(server, webFlagConfig, logger); err != nil { logger.Error(err.Error()) os.Exit(1) } } prometheus-frr-exporter-1.11.0/go.mod000066400000000000000000000025331516444222400175370ustar00rootroot00000000000000module github.com/tynany/frr_exporter go 1.25.0 require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 github.com/prometheus/exporter-toolkit v0.15.1 ) require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) prometheus-frr-exporter-1.11.0/go.sum000066400000000000000000000172651516444222400175740ustar00rootroot00000000000000github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE= github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= prometheus-frr-exporter-1.11.0/internal/000077500000000000000000000000001516444222400202425ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/internal/frrsockets/000077500000000000000000000000001516444222400224275ustar00rootroot00000000000000prometheus-frr-exporter-1.11.0/internal/frrsockets/frrsockets.go000066400000000000000000000044001516444222400251410ustar00rootroot00000000000000package frrsockets import ( "bytes" "fmt" "net" "path/filepath" "time" ) type Connection struct { dirPath string timeout time.Duration } func NewConnection(dirPath string, timeout time.Duration) *Connection { return &Connection{dirPath: dirPath, timeout: timeout} } func (c Connection) ExecBFDCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "bfdd.vty"), cmd, c.timeout) } func (c Connection) ExecBGPCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "bgpd.vty"), cmd, c.timeout) } func (c Connection) ExecOSPFCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "ospfd.vty"), cmd, c.timeout) } func (c Connection) ExecOSPFMultiInstanceCmd(cmd string, instanceID int) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, fmt.Sprintf("ospfd-%d.vty", instanceID)), cmd, c.timeout) } func (c Connection) ExecPIMCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "pimd.vty"), cmd, c.timeout) } func (c Connection) ExecVRRPCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "vrrpd.vty"), cmd, c.timeout) } func (c Connection) ExecZebraCmd(cmd string) ([]byte, error) { return executeCmd(filepath.Join(c.dirPath, "zebra.vty"), cmd, c.timeout) } func executeCmd(socketPath, cmd string, timeout time.Duration) ([]byte, error) { var response bytes.Buffer conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Net: "unix", Name: socketPath}) if err != nil { return nil, err } defer conn.Close() if err = conn.SetDeadline(time.Now().Add(timeout)); err != nil { return nil, err } buf := make([]byte, 4096) // Mimic vtysh by switching to 'enable' mode first. Note that commands need to be // null-terminated. if _, err = conn.Write([]byte("enable\x00")); err != nil { return nil, err } if _, err := conn.Read(buf); err != nil { return nil, err } // Send desired command. if _, err = conn.Write([]byte(cmd + "\x00")); err != nil { return nil, err } for { n, err := conn.Read(buf) if err != nil { return response.Bytes(), err } response.Write(buf[:n]) // frr signals the end of a response with a null character if n > 0 && buf[n-1] == 0 { return bytes.TrimRight(response.Bytes(), "\x00"), nil } } } prometheus-frr-exporter-1.11.0/internal/frrsockets/frrsockets_test.go000066400000000000000000000040531516444222400262040ustar00rootroot00000000000000package frrsockets import ( "net" "os" "path/filepath" "strings" "testing" "time" ) func TestExecuteCmd(t *testing.T) { socketPath := filepath.Join(os.TempDir(), "zebra_mock.vty") expected := "FRRouting 8.1 (localhost).\n" // Simple mock of FRR Zebra Unix socket go mockSocket(socketPath, expected) // Allow socket listener goroutine to settle time.Sleep(100 * time.Millisecond) if resp, err := executeCmd(socketPath, "show version", time.Second); err != nil { t.Fatalf("executeCmd returned error: %v\n", err) } else if string(resp) != expected { t.Fatalf("executeCmd expected '%s', got '%s'\n", expected, resp) } } // TestExecuteCmdWithLargeOutput tests ExecuteCmd when the command returns // a large amount of output exceeding the hard-coded buffer size of 4096. func TestExecuteCmdWithLargeOutput(t *testing.T) { socketPath := filepath.Join(os.TempDir(), "bgp_mock.vty") command := "show a whole lot of data" expected := strings.Repeat("z", 5000) go mockSocket(socketPath, expected) // Allow socket listener goroutine to settle time.Sleep(100 * time.Millisecond) if resp, err := executeCmd(socketPath, command, time.Second); err != nil { t.Fatalf("executeCmd returned error: %v\n", err) } else if string(resp) != expected { t.Fatalf("executeCmd \n expected '%s',\n got '%s'\n", expected, resp) } } func mockSocket(socketPath string, socketData string) { // Simple mock of FRR Unix socket l, err := net.Listen("unix", socketPath) if err != nil { panic(err) } defer os.Remove(socketPath) defer l.Close() conn, err := l.Accept() if err != nil { panic(err) } defer conn.Close() cmd := make([]byte, 1024) if _, err := conn.Read(cmd); err != nil { panic(err) } // If initial command is 'enable', send expected response and wait for next command. if string(cmd[:7]) == "enable\x00" { if _, err := conn.Write([]byte{0, 0, 0, 0}); err != nil { panic(err) } if _, err := conn.Read(cmd); err != nil { panic(err) } } if _, err := conn.Write([]byte(socketData + "\x00")); err != nil { panic(err) } }