Initial commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
birdc.*
|
||||
/bird-exporter
|
||||
debian/.debhelper/
|
||||
debian/.gocache/
|
||||
debian/go/
|
||||
debian/bird-exporter/
|
||||
debian/files
|
||||
debian/*.substvars
|
||||
debian/debhelper-build-stamp
|
||||
debian/*.debhelper
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
30
Makefile
Normal file
30
Makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
.PHONY: all build test clean install sync-version pkg-deb
|
||||
|
||||
BINARY_NAME=bird-exporter
|
||||
INSTALL_PATH=/usr/local/bin
|
||||
|
||||
all: test build
|
||||
|
||||
build:
|
||||
go build -o $(BINARY_NAME) ./cmd/bird-exporter/
|
||||
|
||||
test:
|
||||
go test -v ./cmd/bird-exporter/
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY_NAME)
|
||||
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(BINARY_NAME) debian/files debian/*.substvars debian/debhelper-build-stamp
|
||||
rm -f ../$(BINARY_NAME)_*.deb ../$(BINARY_NAME)_*.changes ../$(BINARY_NAME)_*.buildinfo
|
||||
|
||||
install: build
|
||||
install -m 755 $(BINARY_NAME) $(INSTALL_PATH)/$(BINARY_NAME)
|
||||
|
||||
sync-version:
|
||||
@echo "Syncing version from debian/changelog to main.go..."
|
||||
@VERSION=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
|
||||
sed -i 's/^const Version = ".*"/const Version = "'"$$VERSION"'"/' cmd/bird-exporter/main.go; \
|
||||
echo "Updated Version const to: $$VERSION"
|
||||
|
||||
pkg-deb: sync-version
|
||||
fakeroot dpkg-buildpackage -us -uc -b
|
||||
47
README.md
Normal file
47
README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# BIRD Exporter
|
||||
|
||||
Prometheus exporter for BIRD routing daemon.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
make build
|
||||
|
||||
# Test
|
||||
make test
|
||||
|
||||
# Create Debian package
|
||||
make pkg-deb
|
||||
|
||||
# Install package
|
||||
sudo dpkg -i ../bird-exporter_*.deb
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The Debian package installs a systemd service that reads configuration from `/etc/default/bird-exporter`:
|
||||
|
||||
```bash
|
||||
# Edit service configuration
|
||||
sudo nano /etc/default/bird-exporter
|
||||
|
||||
# Start service
|
||||
sudo systemctl start bird-exporter
|
||||
```
|
||||
|
||||
Default configuration:
|
||||
```
|
||||
BIRD_RUN_USER=bird
|
||||
BIRD_RUN_GROUP=bird
|
||||
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Manual page**: `man bird-exporter` (after package installation)
|
||||
- **Detailed documentation**: [doc/DETAILS.md](doc/DETAILS.md)
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
816
cmd/bird-exporter/main.go
Normal file
816
cmd/bird-exporter/main.go
Normal file
@@ -0,0 +1,816 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const Version = "0.1.0"
|
||||
|
||||
var (
|
||||
listenAddr = flag.String("web.listen-address", ":9324", "Address to listen on for web interface and telemetry.")
|
||||
birdSocket = flag.String("bird.socket", "/var/run/bird/bird.ctl", "Path to BIRD control socket.")
|
||||
scrapePeriod = flag.Duration("period", 60*time.Second, "Period between scrapes of BIRD data.")
|
||||
debug = flag.Bool("debug", false, "Enable debug logging.")
|
||||
)
|
||||
|
||||
var (
|
||||
protocolsMutex sync.RWMutex
|
||||
cachedProtocols []Protocol
|
||||
)
|
||||
|
||||
type Protocol struct {
|
||||
Name string
|
||||
Proto string
|
||||
Table string
|
||||
State string
|
||||
Since string
|
||||
Info string
|
||||
BGPInfo *BGPInfo
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
type BGPInfo struct {
|
||||
BGPState string
|
||||
NeighborAddr string
|
||||
NeighborAS string
|
||||
LocalAS string
|
||||
NeighborID string
|
||||
HoldTimer float64
|
||||
KeepaliveTimer float64
|
||||
SendHoldTimer float64
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Name string
|
||||
State string
|
||||
Table string
|
||||
Preference int
|
||||
InputFilter string
|
||||
OutputFilter string
|
||||
Routes RouteStats
|
||||
ImportStats ChangeStats
|
||||
ExportStats ChangeStats
|
||||
}
|
||||
|
||||
type RouteStats struct {
|
||||
Imported int
|
||||
Filtered int
|
||||
Exported int
|
||||
Preferred int
|
||||
}
|
||||
|
||||
type ChangeStats struct {
|
||||
Updates int
|
||||
Withdraws int
|
||||
Rejected int
|
||||
Filtered int
|
||||
Ignored int
|
||||
Accepted int
|
||||
}
|
||||
|
||||
type BirdCollector struct {
|
||||
protocolUp *prometheus.Desc
|
||||
bgpState *prometheus.Desc
|
||||
routeImported *prometheus.Desc
|
||||
routeFiltered *prometheus.Desc
|
||||
routeExported *prometheus.Desc
|
||||
routePreferred *prometheus.Desc
|
||||
importUpdates *prometheus.Desc
|
||||
importWithdraws *prometheus.Desc
|
||||
importRejected *prometheus.Desc
|
||||
importFiltered *prometheus.Desc
|
||||
importIgnored *prometheus.Desc
|
||||
importAccepted *prometheus.Desc
|
||||
exportUpdates *prometheus.Desc
|
||||
exportWithdraws *prometheus.Desc
|
||||
exportRejected *prometheus.Desc
|
||||
exportFiltered *prometheus.Desc
|
||||
exportAccepted *prometheus.Desc
|
||||
bgpHoldTimer *prometheus.Desc
|
||||
bgpKeepaliveTimer *prometheus.Desc
|
||||
bgpSendHoldTimer *prometheus.Desc
|
||||
}
|
||||
|
||||
func NewBirdCollector() *BirdCollector {
|
||||
return &BirdCollector{
|
||||
protocolUp: prometheus.NewDesc(
|
||||
"bird_protocol_up",
|
||||
"Protocol status (1 = up, 0 = down)",
|
||||
[]string{"name", "proto", "table", "info"},
|
||||
nil,
|
||||
),
|
||||
bgpState: prometheus.NewDesc(
|
||||
"bird_bgp_state",
|
||||
"BGP session state (1 = Established, 0 = other)",
|
||||
[]string{"name", "neighbor_address", "neighbor_as", "local_as", "neighbor_id"},
|
||||
nil,
|
||||
),
|
||||
routeImported: prometheus.NewDesc(
|
||||
"bird_route_imported",
|
||||
"Number of imported routes",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
routeFiltered: prometheus.NewDesc(
|
||||
"bird_route_filtered",
|
||||
"Number of filtered routes",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
routeExported: prometheus.NewDesc(
|
||||
"bird_route_exported",
|
||||
"Number of exported routes",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
routePreferred: prometheus.NewDesc(
|
||||
"bird_route_preferred",
|
||||
"Number of preferred routes",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importUpdates: prometheus.NewDesc(
|
||||
"bird_route_import_updates_total",
|
||||
"Total number of import updates",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importWithdraws: prometheus.NewDesc(
|
||||
"bird_route_import_withdraws_total",
|
||||
"Total number of import withdraws",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importRejected: prometheus.NewDesc(
|
||||
"bird_route_import_rejected_total",
|
||||
"Total number of rejected imports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importFiltered: prometheus.NewDesc(
|
||||
"bird_route_import_filtered_total",
|
||||
"Total number of filtered imports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importIgnored: prometheus.NewDesc(
|
||||
"bird_route_import_ignored_total",
|
||||
"Total number of ignored imports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
importAccepted: prometheus.NewDesc(
|
||||
"bird_route_import_accepted_total",
|
||||
"Total number of accepted imports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
exportUpdates: prometheus.NewDesc(
|
||||
"bird_route_export_updates_total",
|
||||
"Total number of export updates",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
exportWithdraws: prometheus.NewDesc(
|
||||
"bird_route_export_withdraws_total",
|
||||
"Total number of export withdraws",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
exportRejected: prometheus.NewDesc(
|
||||
"bird_route_export_rejected_total",
|
||||
"Total number of rejected exports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
exportFiltered: prometheus.NewDesc(
|
||||
"bird_route_export_filtered_total",
|
||||
"Total number of filtered exports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
exportAccepted: prometheus.NewDesc(
|
||||
"bird_route_export_accepted_total",
|
||||
"Total number of accepted exports",
|
||||
[]string{"name", "proto", "channel", "table"},
|
||||
nil,
|
||||
),
|
||||
bgpHoldTimer: prometheus.NewDesc(
|
||||
"bird_bgp_hold_timer_seconds",
|
||||
"BGP hold timer in seconds",
|
||||
[]string{"name", "neighbor_address"},
|
||||
nil,
|
||||
),
|
||||
bgpKeepaliveTimer: prometheus.NewDesc(
|
||||
"bird_bgp_keepalive_timer_seconds",
|
||||
"BGP keepalive timer in seconds",
|
||||
[]string{"name", "neighbor_address"},
|
||||
nil,
|
||||
),
|
||||
bgpSendHoldTimer: prometheus.NewDesc(
|
||||
"bird_bgp_send_hold_timer_seconds",
|
||||
"BGP send hold timer in seconds",
|
||||
[]string{"name", "neighbor_address"},
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BirdCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.protocolUp
|
||||
ch <- c.bgpState
|
||||
ch <- c.routeImported
|
||||
ch <- c.routeFiltered
|
||||
ch <- c.routeExported
|
||||
ch <- c.routePreferred
|
||||
ch <- c.importUpdates
|
||||
ch <- c.importWithdraws
|
||||
ch <- c.importRejected
|
||||
ch <- c.importFiltered
|
||||
ch <- c.importIgnored
|
||||
ch <- c.importAccepted
|
||||
ch <- c.exportUpdates
|
||||
ch <- c.exportWithdraws
|
||||
ch <- c.exportRejected
|
||||
ch <- c.exportFiltered
|
||||
ch <- c.exportAccepted
|
||||
ch <- c.bgpHoldTimer
|
||||
ch <- c.bgpKeepaliveTimer
|
||||
ch <- c.bgpSendHoldTimer
|
||||
}
|
||||
|
||||
func (c *BirdCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
protocolsMutex.RLock()
|
||||
protocols := cachedProtocols
|
||||
protocolsMutex.RUnlock()
|
||||
|
||||
for _, p := range protocols {
|
||||
// Protocol status
|
||||
upValue := 0.0
|
||||
if strings.ToLower(p.State) == "up" {
|
||||
upValue = 1.0
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.protocolUp,
|
||||
prometheus.GaugeValue,
|
||||
upValue,
|
||||
p.Name, p.Proto, p.Table, p.Info,
|
||||
)
|
||||
|
||||
// BGP specific metrics
|
||||
if p.BGPInfo != nil {
|
||||
bgpEstablished := 0.0
|
||||
if p.BGPInfo.BGPState == "Established" {
|
||||
bgpEstablished = 1.0
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bgpState,
|
||||
prometheus.GaugeValue,
|
||||
bgpEstablished,
|
||||
p.Name, p.BGPInfo.NeighborAddr, p.BGPInfo.NeighborAS, p.BGPInfo.LocalAS, p.BGPInfo.NeighborID,
|
||||
)
|
||||
|
||||
if p.BGPInfo.HoldTimer > 0 {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bgpHoldTimer,
|
||||
prometheus.GaugeValue,
|
||||
p.BGPInfo.HoldTimer,
|
||||
p.Name, p.BGPInfo.NeighborAddr,
|
||||
)
|
||||
}
|
||||
|
||||
if p.BGPInfo.KeepaliveTimer > 0 {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bgpKeepaliveTimer,
|
||||
prometheus.GaugeValue,
|
||||
p.BGPInfo.KeepaliveTimer,
|
||||
p.Name, p.BGPInfo.NeighborAddr,
|
||||
)
|
||||
}
|
||||
|
||||
if p.BGPInfo.SendHoldTimer > 0 {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bgpSendHoldTimer,
|
||||
prometheus.GaugeValue,
|
||||
p.BGPInfo.SendHoldTimer,
|
||||
p.Name, p.BGPInfo.NeighborAddr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Channel metrics
|
||||
for _, channel := range p.Channels {
|
||||
labels := []string{p.Name, p.Proto, channel.Name, channel.Table}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.routeImported,
|
||||
prometheus.GaugeValue,
|
||||
float64(channel.Routes.Imported),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.routeFiltered,
|
||||
prometheus.GaugeValue,
|
||||
float64(channel.Routes.Filtered),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.routeExported,
|
||||
prometheus.GaugeValue,
|
||||
float64(channel.Routes.Exported),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.routePreferred,
|
||||
prometheus.GaugeValue,
|
||||
float64(channel.Routes.Preferred),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importUpdates,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Updates),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importWithdraws,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Withdraws),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importRejected,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Rejected),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importFiltered,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Filtered),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importIgnored,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Ignored),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.importAccepted,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ImportStats.Accepted),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.exportUpdates,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ExportStats.Updates),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.exportWithdraws,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ExportStats.Withdraws),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.exportRejected,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ExportStats.Rejected),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.exportFiltered,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ExportStats.Filtered),
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.exportAccepted,
|
||||
prometheus.CounterValue,
|
||||
float64(channel.ExportStats.Accepted),
|
||||
labels...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getBirdOutput() (io.ReadCloser, error) {
|
||||
// Connect to BIRD socket
|
||||
conn, err := net.Dial("unix", *birdSocket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD socket: %w", err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// Read welcome message - BIRD sends "0001 BIRD x.y.z ready."
|
||||
// Lines ending with space (not dash) are the last line of a response
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to read welcome message: %w", err)
|
||||
}
|
||||
// Welcome message ends with "0001 " (code + space)
|
||||
// Check if line starts with 4 digits followed by space
|
||||
if len(line) >= 5 &&
|
||||
line[0] >= '0' && line[0] <= '9' &&
|
||||
line[1] >= '0' && line[1] <= '9' &&
|
||||
line[2] >= '0' && line[2] <= '9' &&
|
||||
line[3] >= '0' && line[3] <= '9' &&
|
||||
line[4] == ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Send the command
|
||||
command := "show protocols all"
|
||||
if *debug {
|
||||
log.Printf("BIRD: %s", command)
|
||||
}
|
||||
_, err = fmt.Fprintf(conn, "%s\n", command)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to send command: %w", err)
|
||||
}
|
||||
|
||||
// Read and strip BIRD protocol codes from the response
|
||||
var output strings.Builder
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// BIRD protocol format: "CODE-text" or "CODE text" or "CODE"
|
||||
// CODE is 4 digits, dash means more lines follow, space means last line
|
||||
if len(line) >= 4 &&
|
||||
line[0] >= '0' && line[0] <= '9' &&
|
||||
line[1] >= '0' && line[1] <= '9' &&
|
||||
line[2] >= '0' && line[2] <= '9' &&
|
||||
line[3] >= '0' && line[3] <= '9' {
|
||||
|
||||
// 0000 indicates end of output
|
||||
if line[0:4] == "0000" {
|
||||
break
|
||||
}
|
||||
|
||||
// Skip lines that are just status codes (header/footer)
|
||||
if len(line) == 5 && (line[4] == '\n' || line[4] == ' ') {
|
||||
continue
|
||||
}
|
||||
|
||||
// If line has content after code and separator, strip the code
|
||||
if len(line) > 5 && (line[4] == '-' || line[4] == ' ') {
|
||||
// Remove the "CODE-" or "CODE " prefix
|
||||
output.WriteString(line[5:])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Lines without codes (continuation lines) - write as-is
|
||||
output.WriteString(line)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
|
||||
// Return a reader for the stripped output
|
||||
return io.NopCloser(strings.NewReader(output.String())), nil
|
||||
}
|
||||
|
||||
func parseBirdOutput(input io.Reader) ([]Protocol, error) {
|
||||
var protocols []Protocol
|
||||
scanner := bufio.NewScanner(input)
|
||||
|
||||
var currentProto *Protocol
|
||||
var currentChannel *Channel
|
||||
var inChannelStats bool
|
||||
|
||||
// Regex patterns
|
||||
// Match protocol line - Since field can be "YYYY-MM-DD HH:MM:SS" or just "HH:MM:SS.xxx"
|
||||
protoLineRe := regexp.MustCompile(`^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+(?:\s+\S+)?)\s*(.*)$`)
|
||||
channelRe := regexp.MustCompile(`^\s+Channel (\S+)`)
|
||||
bgpStateRe := regexp.MustCompile(`^\s+BGP state:\s+(.+)`)
|
||||
neighborAddrRe := regexp.MustCompile(`^\s+Neighbor address:\s+(.+)`)
|
||||
neighborASRe := regexp.MustCompile(`^\s+Neighbor AS:\s+(\d+)`)
|
||||
localASRe := regexp.MustCompile(`^\s+Local AS:\s+(\d+)`)
|
||||
neighborIDRe := regexp.MustCompile(`^\s+Neighbor ID:\s+(.+)`)
|
||||
holdTimerRe := regexp.MustCompile(`^\s+Hold timer:\s+([\d.]+)/([\d.]+)`)
|
||||
keepaliveTimerRe := regexp.MustCompile(`^\s+Keepalive timer:\s+([\d.]+)/([\d.]+)`)
|
||||
sendHoldTimerRe := regexp.MustCompile(`^\s+Send hold timer:\s+([\d.]+)/([\d.]+)`)
|
||||
stateRe := regexp.MustCompile(`^\s+State:\s+(.+)`)
|
||||
tableRe := regexp.MustCompile(`^\s+Table:\s+(.+)`)
|
||||
preferenceRe := regexp.MustCompile(`^\s+Preference:\s+(\d+)`)
|
||||
inputFilterRe := regexp.MustCompile(`^\s+Input filter:\s+(.+)`)
|
||||
outputFilterRe := regexp.MustCompile(`^\s+Output filter:\s+(.+)`)
|
||||
// Routes line can be either:
|
||||
// "Routes: X imported, Y exported, Z preferred"
|
||||
// "Routes: X imported, Y filtered, Z exported, W preferred"
|
||||
routesRe := regexp.MustCompile(`^\s+Routes:\s+(\d+) imported,(?:\s+(\d+) filtered,)?\s+(\d+) exported,\s+(\d+) preferred`)
|
||||
importStatsRe := regexp.MustCompile(`^\s+Import updates:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)`)
|
||||
importWithdrawsRe := regexp.MustCompile(`^\s+Import withdraws:\s+(\d+)\s+(\d+)\s+---\s+(\d+)\s+(\d+)`)
|
||||
exportStatsRe := regexp.MustCompile(`^\s+Export updates:\s+(\d+)\s+(\d+)\s+(\d+)\s+---\s+(\d+)`)
|
||||
exportWithdrawsRe := regexp.MustCompile(`^\s+Export withdraws:\s+(\d+)\s+---\s+---\s+---\s+(\d+)`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Skip header lines
|
||||
if strings.HasPrefix(line, "BIRD") || strings.HasPrefix(line, "Name") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty line often signals end of protocol block
|
||||
if strings.TrimSpace(line) == "" {
|
||||
if currentProto != nil && currentChannel != nil {
|
||||
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||
currentChannel = nil
|
||||
}
|
||||
inChannelStats = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for new protocol
|
||||
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
|
||||
// Save previous protocol
|
||||
if currentProto != nil {
|
||||
if currentChannel != nil {
|
||||
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||
currentChannel = nil
|
||||
}
|
||||
protocols = append(protocols, *currentProto)
|
||||
}
|
||||
|
||||
// Parse new protocol line
|
||||
matches := protoLineRe.FindStringSubmatch(line)
|
||||
if len(matches) > 6 {
|
||||
currentProto = &Protocol{
|
||||
Name: matches[1],
|
||||
Proto: matches[2],
|
||||
Table: matches[3],
|
||||
State: matches[4],
|
||||
Since: matches[5],
|
||||
Info: strings.TrimSpace(matches[6]),
|
||||
}
|
||||
|
||||
if currentProto.Proto == "BGP" {
|
||||
currentProto.BGPInfo = &BGPInfo{}
|
||||
}
|
||||
}
|
||||
inChannelStats = false
|
||||
continue
|
||||
}
|
||||
|
||||
if currentProto == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse BGP specific info
|
||||
if matches := bgpStateRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
currentProto.BGPInfo.BGPState = strings.TrimSpace(matches[1])
|
||||
}
|
||||
} else if matches := neighborAddrRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
currentProto.BGPInfo.NeighborAddr = strings.TrimSpace(matches[1])
|
||||
}
|
||||
} else if matches := neighborASRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
currentProto.BGPInfo.NeighborAS = matches[1]
|
||||
}
|
||||
} else if matches := localASRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
currentProto.BGPInfo.LocalAS = matches[1]
|
||||
}
|
||||
} else if matches := neighborIDRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
currentProto.BGPInfo.NeighborID = strings.TrimSpace(matches[1])
|
||||
}
|
||||
} else if matches := holdTimerRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||
currentProto.BGPInfo.HoldTimer = val
|
||||
}
|
||||
}
|
||||
} else if matches := keepaliveTimerRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||
currentProto.BGPInfo.KeepaliveTimer = val
|
||||
}
|
||||
}
|
||||
} else if matches := sendHoldTimerRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentProto.BGPInfo != nil {
|
||||
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||
currentProto.BGPInfo.SendHoldTimer = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for channel start
|
||||
if matches := channelRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentChannel != nil {
|
||||
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||
}
|
||||
currentChannel = &Channel{
|
||||
Name: matches[1],
|
||||
}
|
||||
inChannelStats = false
|
||||
continue
|
||||
}
|
||||
|
||||
if currentChannel == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse channel properties
|
||||
if matches := stateRe.FindStringSubmatch(line); matches != nil {
|
||||
currentChannel.State = strings.TrimSpace(matches[1])
|
||||
} else if matches := tableRe.FindStringSubmatch(line); matches != nil {
|
||||
currentChannel.Table = strings.TrimSpace(matches[1])
|
||||
} else if matches := preferenceRe.FindStringSubmatch(line); matches != nil {
|
||||
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.Preference = val
|
||||
}
|
||||
} else if matches := inputFilterRe.FindStringSubmatch(line); matches != nil {
|
||||
currentChannel.InputFilter = strings.TrimSpace(matches[1])
|
||||
} else if matches := outputFilterRe.FindStringSubmatch(line); matches != nil {
|
||||
currentChannel.OutputFilter = strings.TrimSpace(matches[1])
|
||||
} else if matches := routesRe.FindStringSubmatch(line); matches != nil {
|
||||
// matches[1] = imported
|
||||
// matches[2] = filtered (optional, may be empty)
|
||||
// matches[3] = exported
|
||||
// matches[4] = preferred
|
||||
if imported, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.Routes.Imported = imported
|
||||
}
|
||||
if matches[2] != "" {
|
||||
if filtered, err := strconv.Atoi(matches[2]); err == nil {
|
||||
currentChannel.Routes.Filtered = filtered
|
||||
}
|
||||
}
|
||||
if exported, err := strconv.Atoi(matches[3]); err == nil {
|
||||
currentChannel.Routes.Exported = exported
|
||||
}
|
||||
if preferred, err := strconv.Atoi(matches[4]); err == nil {
|
||||
currentChannel.Routes.Preferred = preferred
|
||||
}
|
||||
} else if strings.Contains(line, "Route change stats:") {
|
||||
inChannelStats = true
|
||||
} else if inChannelStats {
|
||||
if matches := importStatsRe.FindStringSubmatch(line); matches != nil {
|
||||
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.ImportStats.Updates = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[2]); err == nil {
|
||||
currentChannel.ImportStats.Rejected = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[3]); err == nil {
|
||||
currentChannel.ImportStats.Filtered = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[4]); err == nil {
|
||||
currentChannel.ImportStats.Ignored = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[5]); err == nil {
|
||||
currentChannel.ImportStats.Accepted = val
|
||||
}
|
||||
} else if matches := importWithdrawsRe.FindStringSubmatch(line); matches != nil {
|
||||
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.ImportStats.Withdraws = val
|
||||
}
|
||||
} else if matches := exportStatsRe.FindStringSubmatch(line); matches != nil {
|
||||
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.ExportStats.Updates = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[2]); err == nil {
|
||||
currentChannel.ExportStats.Rejected = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[3]); err == nil {
|
||||
currentChannel.ExportStats.Filtered = val
|
||||
}
|
||||
if val, err := strconv.Atoi(matches[4]); err == nil {
|
||||
currentChannel.ExportStats.Accepted = val
|
||||
}
|
||||
} else if matches := exportWithdrawsRe.FindStringSubmatch(line); matches != nil {
|
||||
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||
currentChannel.ExportStats.Withdraws = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last protocol
|
||||
if currentProto != nil {
|
||||
if currentChannel != nil {
|
||||
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||
}
|
||||
protocols = append(protocols, *currentProto)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
|
||||
return protocols, nil
|
||||
}
|
||||
|
||||
func scrapeOnce() error {
|
||||
input, err := getBirdOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get BIRD output: %w", err)
|
||||
}
|
||||
defer input.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse BIRD output: %w", err)
|
||||
}
|
||||
|
||||
protocolsMutex.Lock()
|
||||
cachedProtocols = protocols
|
||||
protocolsMutex.Unlock()
|
||||
|
||||
if *debug {
|
||||
log.Printf("Successfully scraped %d protocols", len(protocols))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startPeriodicScraper() {
|
||||
ticker := time.NewTicker(*scrapePeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := scrapeOnce(); err != nil {
|
||||
log.Printf("Error scraping BIRD data: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
log.Printf("BIRD Exporter version %s", Version)
|
||||
|
||||
// Perform initial scrape
|
||||
if *debug {
|
||||
log.Printf("Performing initial scrape...")
|
||||
}
|
||||
if err := scrapeOnce(); err != nil {
|
||||
log.Fatalf("Initial scrape failed: %v", err)
|
||||
}
|
||||
|
||||
// Start periodic scraper in background
|
||||
log.Printf("Starting periodic scraper with period %s", *scrapePeriod)
|
||||
go startPeriodicScraper()
|
||||
|
||||
collector := NewBirdCollector()
|
||||
prometheus.MustRegister(collector)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`<html>
|
||||
<head><title>BIRD Exporter</title></head>
|
||||
<body>
|
||||
<h1>BIRD Exporter</h1>
|
||||
<p><a href="/metrics">Metrics</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
})
|
||||
|
||||
log.Printf("Starting BIRD exporter on %s", *listenAddr)
|
||||
log.Fatal(http.ListenAndServe(*listenAddr, nil))
|
||||
}
|
||||
416
cmd/bird-exporter/main_test.go
Normal file
416
cmd/bird-exporter/main_test.go
Normal file
@@ -0,0 +1,416 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseBirdOutput(t *testing.T) {
|
||||
file, err := os.Open("testdata/sample_output.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
// Check we got the expected number of protocols
|
||||
expectedProtocols := 8 // device1, direct1, kernel4, static4, ospf4, fogixp_47498_ipv4_3, fogixp_47498_ipv6_1, fogixp_47498_ipv6_2
|
||||
if len(protocols) != expectedProtocols {
|
||||
t.Errorf("Expected %d protocols, got %d", expectedProtocols, len(protocols))
|
||||
}
|
||||
|
||||
// Test Device protocol
|
||||
device := findProtocol(protocols, "device1")
|
||||
if device == nil {
|
||||
t.Fatal("device1 protocol not found")
|
||||
}
|
||||
if device.Proto != "Device" {
|
||||
t.Errorf("Expected proto 'Device', got '%s'", device.Proto)
|
||||
}
|
||||
if device.State != "up" {
|
||||
t.Errorf("Expected state 'up', got '%s'", device.State)
|
||||
}
|
||||
if device.Table != "---" {
|
||||
t.Errorf("Expected table '---', got '%s'", device.Table)
|
||||
}
|
||||
|
||||
// Test Direct protocol with channels
|
||||
direct := findProtocol(protocols, "direct1")
|
||||
if direct == nil {
|
||||
t.Fatal("direct1 protocol not found")
|
||||
}
|
||||
if direct.Proto != "Direct" {
|
||||
t.Errorf("Expected proto 'Direct', got '%s'", direct.Proto)
|
||||
}
|
||||
if len(direct.Channels) != 2 {
|
||||
t.Errorf("Expected 2 channels, got %d", len(direct.Channels))
|
||||
}
|
||||
|
||||
// Test direct1 ipv4 channel
|
||||
ipv4Chan := findChannel(direct.Channels, "ipv4")
|
||||
if ipv4Chan == nil {
|
||||
t.Fatal("ipv4 channel not found in direct1")
|
||||
}
|
||||
if ipv4Chan.State != "UP" {
|
||||
t.Errorf("Expected channel state 'UP', got '%s'", ipv4Chan.State)
|
||||
}
|
||||
if ipv4Chan.Table != "master4" {
|
||||
t.Errorf("Expected table 'master4', got '%s'", ipv4Chan.Table)
|
||||
}
|
||||
if ipv4Chan.Preference != 240 {
|
||||
t.Errorf("Expected preference 240, got %d", ipv4Chan.Preference)
|
||||
}
|
||||
if ipv4Chan.InputFilter != "ACCEPT" {
|
||||
t.Errorf("Expected input filter 'ACCEPT', got '%s'", ipv4Chan.InputFilter)
|
||||
}
|
||||
if ipv4Chan.OutputFilter != "REJECT" {
|
||||
t.Errorf("Expected output filter 'REJECT', got '%s'", ipv4Chan.OutputFilter)
|
||||
}
|
||||
|
||||
// Test route stats
|
||||
if ipv4Chan.Routes.Imported != 10 {
|
||||
t.Errorf("Expected 10 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||
}
|
||||
if ipv4Chan.Routes.Exported != 0 {
|
||||
t.Errorf("Expected 0 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||
}
|
||||
if ipv4Chan.Routes.Preferred != 10 {
|
||||
t.Errorf("Expected 10 preferred routes, got %d", ipv4Chan.Routes.Preferred)
|
||||
}
|
||||
|
||||
// Test import stats
|
||||
if ipv4Chan.ImportStats.Updates != 155 {
|
||||
t.Errorf("Expected 155 import updates, got %d", ipv4Chan.ImportStats.Updates)
|
||||
}
|
||||
if ipv4Chan.ImportStats.Rejected != 0 {
|
||||
t.Errorf("Expected 0 import rejected, got %d", ipv4Chan.ImportStats.Rejected)
|
||||
}
|
||||
if ipv4Chan.ImportStats.Filtered != 0 {
|
||||
t.Errorf("Expected 0 import filtered, got %d", ipv4Chan.ImportStats.Filtered)
|
||||
}
|
||||
if ipv4Chan.ImportStats.Ignored != 0 {
|
||||
t.Errorf("Expected 0 import ignored, got %d", ipv4Chan.ImportStats.Ignored)
|
||||
}
|
||||
if ipv4Chan.ImportStats.Accepted != 155 {
|
||||
t.Errorf("Expected 155 import accepted, got %d", ipv4Chan.ImportStats.Accepted)
|
||||
}
|
||||
if ipv4Chan.ImportStats.Withdraws != 153 {
|
||||
t.Errorf("Expected 153 import withdraws, got %d", ipv4Chan.ImportStats.Withdraws)
|
||||
}
|
||||
|
||||
// Test export stats
|
||||
if ipv4Chan.ExportStats.Updates != 0 {
|
||||
t.Errorf("Expected 0 export updates, got %d", ipv4Chan.ExportStats.Updates)
|
||||
}
|
||||
if ipv4Chan.ExportStats.Withdraws != 0 {
|
||||
t.Errorf("Expected 0 export withdraws, got %d", ipv4Chan.ExportStats.Withdraws)
|
||||
}
|
||||
|
||||
// Test direct1 ipv6 channel
|
||||
ipv6Chan := findChannel(direct.Channels, "ipv6")
|
||||
if ipv6Chan == nil {
|
||||
t.Fatal("ipv6 channel not found in direct1")
|
||||
}
|
||||
if ipv6Chan.Routes.Imported != 5 {
|
||||
t.Errorf("Expected 5 imported routes, got %d", ipv6Chan.Routes.Imported)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBGPProtocol(t *testing.T) {
|
||||
file, err := os.Open("testdata/sample_output.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
// Test BGP protocol (established)
|
||||
bgp := findProtocol(protocols, "fogixp_47498_ipv4_3")
|
||||
if bgp == nil {
|
||||
t.Fatal("fogixp_47498_ipv4_3 protocol not found")
|
||||
}
|
||||
if bgp.Proto != "BGP" {
|
||||
t.Errorf("Expected proto 'BGP', got '%s'", bgp.Proto)
|
||||
}
|
||||
if bgp.State != "up" {
|
||||
t.Errorf("Expected state 'up', got '%s'", bgp.State)
|
||||
}
|
||||
if bgp.Info != "Established" {
|
||||
t.Errorf("Expected info 'Established', got '%s'", bgp.Info)
|
||||
}
|
||||
|
||||
// Test BGP info
|
||||
if bgp.BGPInfo == nil {
|
||||
t.Fatal("BGP info is nil")
|
||||
}
|
||||
if bgp.BGPInfo.BGPState != "Established" {
|
||||
t.Errorf("Expected BGP state 'Established', got '%s'", bgp.BGPInfo.BGPState)
|
||||
}
|
||||
if bgp.BGPInfo.NeighborAddr != "185.1.147.3" {
|
||||
t.Errorf("Expected neighbor address '185.1.147.3', got '%s'", bgp.BGPInfo.NeighborAddr)
|
||||
}
|
||||
if bgp.BGPInfo.NeighborAS != "47498" {
|
||||
t.Errorf("Expected neighbor AS '47498', got '%s'", bgp.BGPInfo.NeighborAS)
|
||||
}
|
||||
if bgp.BGPInfo.LocalAS != "8298" {
|
||||
t.Errorf("Expected local AS '8298', got '%s'", bgp.BGPInfo.LocalAS)
|
||||
}
|
||||
if bgp.BGPInfo.NeighborID != "185.1.147.3" {
|
||||
t.Errorf("Expected neighbor ID '185.1.147.3', got '%s'", bgp.BGPInfo.NeighborID)
|
||||
}
|
||||
|
||||
// Test BGP timers
|
||||
if bgp.BGPInfo.HoldTimer != 174.765 {
|
||||
t.Errorf("Expected hold timer 174.765, got %f", bgp.BGPInfo.HoldTimer)
|
||||
}
|
||||
if bgp.BGPInfo.KeepaliveTimer != 30.587 {
|
||||
t.Errorf("Expected keepalive timer 30.587, got %f", bgp.BGPInfo.KeepaliveTimer)
|
||||
}
|
||||
if bgp.BGPInfo.SendHoldTimer != 425.866 {
|
||||
t.Errorf("Expected send hold timer 425.866, got %f", bgp.BGPInfo.SendHoldTimer)
|
||||
}
|
||||
|
||||
// Test BGP channel
|
||||
if len(bgp.Channels) != 1 {
|
||||
t.Errorf("Expected 1 channel, got %d", len(bgp.Channels))
|
||||
}
|
||||
bgpChan := findChannel(bgp.Channels, "ipv4")
|
||||
if bgpChan == nil {
|
||||
t.Fatal("ipv4 channel not found in fogixp_47498_ipv4_3")
|
||||
}
|
||||
if bgpChan.Routes.Imported != 44589 {
|
||||
t.Errorf("Expected 44589 imported routes, got %d", bgpChan.Routes.Imported)
|
||||
}
|
||||
if bgpChan.Routes.Filtered != 6 {
|
||||
t.Errorf("Expected 6 filtered routes, got %d", bgpChan.Routes.Filtered)
|
||||
}
|
||||
if bgpChan.Routes.Exported != 27 {
|
||||
t.Errorf("Expected 27 exported routes, got %d", bgpChan.Routes.Exported)
|
||||
}
|
||||
if bgpChan.Routes.Preferred != 4173 {
|
||||
t.Errorf("Expected 4173 preferred routes, got %d", bgpChan.Routes.Preferred)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKernelProtocol(t *testing.T) {
|
||||
file, err := os.Open("testdata/sample_output.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
kernel := findProtocol(protocols, "kernel4")
|
||||
if kernel == nil {
|
||||
t.Fatal("kernel4 protocol not found")
|
||||
}
|
||||
|
||||
if len(kernel.Channels) != 1 {
|
||||
t.Fatalf("Expected 1 channel, got %d", len(kernel.Channels))
|
||||
}
|
||||
|
||||
ipv4Chan := kernel.Channels[0]
|
||||
if ipv4Chan.Routes.Exported != 1044496 {
|
||||
t.Errorf("Expected 1044496 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||
}
|
||||
if ipv4Chan.ExportStats.Updates != 1684718679 {
|
||||
t.Errorf("Expected 1684718679 export updates, got %d", ipv4Chan.ExportStats.Updates)
|
||||
}
|
||||
if ipv4Chan.ExportStats.Filtered != 143 {
|
||||
t.Errorf("Expected 143 export filtered, got %d", ipv4Chan.ExportStats.Filtered)
|
||||
}
|
||||
if ipv4Chan.ExportStats.Accepted != 1684718536 {
|
||||
t.Errorf("Expected 1684718536 export accepted, got %d", ipv4Chan.ExportStats.Accepted)
|
||||
}
|
||||
if ipv4Chan.ExportStats.Withdraws != 37968736 {
|
||||
t.Errorf("Expected 37968736 export withdraws, got %d", ipv4Chan.ExportStats.Withdraws)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOSPFProtocol(t *testing.T) {
|
||||
file, err := os.Open("testdata/sample_output.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
ospf := findProtocol(protocols, "ospf4")
|
||||
if ospf == nil {
|
||||
t.Fatal("ospf4 protocol not found")
|
||||
}
|
||||
|
||||
if ospf.Proto != "OSPF" {
|
||||
t.Errorf("Expected proto 'OSPF', got '%s'", ospf.Proto)
|
||||
}
|
||||
if ospf.Info != "Running" {
|
||||
t.Errorf("Expected info 'Running', got '%s'", ospf.Info)
|
||||
}
|
||||
|
||||
if len(ospf.Channels) != 1 {
|
||||
t.Fatalf("Expected 1 channel, got %d", len(ospf.Channels))
|
||||
}
|
||||
|
||||
ipv4Chan := ospf.Channels[0]
|
||||
if ipv4Chan.InputFilter != "f_ospf" {
|
||||
t.Errorf("Expected input filter 'f_ospf', got '%s'", ipv4Chan.InputFilter)
|
||||
}
|
||||
if ipv4Chan.Routes.Imported != 26 {
|
||||
t.Errorf("Expected 26 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||
}
|
||||
if ipv4Chan.Routes.Exported != 4 {
|
||||
t.Errorf("Expected 4 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||
}
|
||||
if ipv4Chan.Routes.Preferred != 25 {
|
||||
t.Errorf("Expected 25 preferred routes, got %d", ipv4Chan.Routes.Preferred)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStaticProtocol(t *testing.T) {
|
||||
file, err := os.Open("testdata/sample_output.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
static := findProtocol(protocols, "static4")
|
||||
if static == nil {
|
||||
t.Fatal("static4 protocol not found")
|
||||
}
|
||||
|
||||
if static.Proto != "Static" {
|
||||
t.Errorf("Expected proto 'Static', got '%s'", static.Proto)
|
||||
}
|
||||
|
||||
if len(static.Channels) != 1 {
|
||||
t.Fatalf("Expected 1 channel, got %d", len(static.Channels))
|
||||
}
|
||||
|
||||
ipv4Chan := static.Channels[0]
|
||||
if ipv4Chan.Preference != 200 {
|
||||
t.Errorf("Expected preference 200, got %d", ipv4Chan.Preference)
|
||||
}
|
||||
if ipv4Chan.Routes.Imported != 3 {
|
||||
t.Errorf("Expected 3 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBirdOutputWithRealFile(t *testing.T) {
|
||||
// Test with the actual birdc.show.proto.all file if it exists
|
||||
if _, err := os.Stat("../../birdc.show.proto.all"); os.IsNotExist(err) {
|
||||
t.Skip("Real birdc.show.proto.all file not found, skipping")
|
||||
}
|
||||
|
||||
file, err := os.Open("../../birdc.show.proto.all")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open real file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed on real file: %v", err)
|
||||
}
|
||||
|
||||
if len(protocols) == 0 {
|
||||
t.Error("Expected at least one protocol from real file")
|
||||
}
|
||||
|
||||
// Check that we have various protocol types
|
||||
hasDevice := false
|
||||
hasBGP := false
|
||||
for _, p := range protocols {
|
||||
if p.Proto == "Device" {
|
||||
hasDevice = true
|
||||
}
|
||||
if p.Proto == "BGP" {
|
||||
hasBGP = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDevice {
|
||||
t.Error("Expected to find at least one Device protocol")
|
||||
}
|
||||
if !hasBGP {
|
||||
t.Error("Expected to find at least one BGP protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmptyFile(t *testing.T) {
|
||||
// Create a temporary empty file
|
||||
tmpFile, err := os.CreateTemp("", "empty-*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
file, err := os.Open(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open temp file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
protocols, err := parseBirdOutput(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(protocols) != 0 {
|
||||
t.Errorf("Expected 0 protocols from empty file, got %d", len(protocols))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileNotFound(t *testing.T) {
|
||||
_, err := os.Open("/tmp/does-not-exist-bird-test-12345.txt")
|
||||
if err == nil {
|
||||
t.Error("Expected error when opening non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func findProtocol(protocols []Protocol, name string) *Protocol {
|
||||
for i := range protocols {
|
||||
if protocols[i].Name == name {
|
||||
return &protocols[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findChannel(channels []Channel, name string) *Channel {
|
||||
for i := range channels {
|
||||
if channels[i].Name == name {
|
||||
return &channels[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestMain to handle flag parsing for tests
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
204
cmd/bird-exporter/testdata/sample_output.txt
vendored
Normal file
204
cmd/bird-exporter/testdata/sample_output.txt
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
BIRD v2.15.1-4-g280daed5-x ready.
|
||||
Name Proto Table State Since Info
|
||||
device1 Device --- up 2024-04-16 21:05:38
|
||||
|
||||
direct1 Direct --- up 2024-04-16 21:05:38
|
||||
Channel ipv4
|
||||
State: UP
|
||||
Table: master4
|
||||
Preference: 240
|
||||
Input filter: ACCEPT
|
||||
Output filter: REJECT
|
||||
Routes: 10 imported, 0 exported, 10 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 155 0 0 0 155
|
||||
Import withdraws: 153 0 --- 8 145
|
||||
Export updates: 0 0 0 --- 0
|
||||
Export withdraws: 0 --- --- --- 0
|
||||
Channel ipv6
|
||||
State: UP
|
||||
Table: master6
|
||||
Preference: 240
|
||||
Input filter: ACCEPT
|
||||
Output filter: REJECT
|
||||
Routes: 5 imported, 0 exported, 5 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 140 0 0 0 140
|
||||
Import withdraws: 138 0 --- 8 130
|
||||
Export updates: 0 0 0 --- 0
|
||||
Export withdraws: 0 --- --- --- 0
|
||||
|
||||
kernel4 Kernel master4 up 2024-04-16 21:05:38
|
||||
Channel ipv4
|
||||
State: UP
|
||||
Table: master4
|
||||
Preference: 10
|
||||
Input filter: REJECT
|
||||
Output filter: (unnamed)
|
||||
Routes: 0 imported, 1044496 exported, 0 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 0 0 0 0 0
|
||||
Import withdraws: 0 0 --- 0 0
|
||||
Export updates: 1684718679 0 143 --- 1684718536
|
||||
Export withdraws: 37968736 --- --- --- 37968651
|
||||
|
||||
static4 Static master4 up 2024-04-16 21:05:38
|
||||
Channel ipv4
|
||||
State: UP
|
||||
Table: master4
|
||||
Preference: 200
|
||||
Input filter: ACCEPT
|
||||
Output filter: ACCEPT
|
||||
Routes: 3 imported, 0 exported, 3 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 4 0 0 0 4
|
||||
Import withdraws: 1 0 --- 0 1
|
||||
Export updates: 0 0 0 --- 0
|
||||
Export withdraws: 0 --- --- --- 0
|
||||
|
||||
ospf4 OSPF master4 up 2024-06-19 19:34:13 Running
|
||||
Channel ipv4
|
||||
State: UP
|
||||
Table: master4
|
||||
Preference: 150
|
||||
Input filter: f_ospf
|
||||
Output filter: f_ospf
|
||||
Routes: 26 imported, 4 exported, 25 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 11565 0 0 0 11565
|
||||
Import withdraws: 912 0 --- 0 912
|
||||
Export updates: 1521957177 10903 1521945947 --- 327
|
||||
Export withdraws: 35569447 --- --- --- 72
|
||||
|
||||
fogixp_47498_ipv4_3 BGP --- up 2025-12-29 15:33:26 Established
|
||||
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||
BGP state: Established
|
||||
Neighbor address: 185.1.147.3
|
||||
Neighbor AS: 47498
|
||||
Local AS: 8298
|
||||
Neighbor ID: 185.1.147.3
|
||||
Local capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv4
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Neighbor capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv4
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Role: rs_server
|
||||
Session: external AS4
|
||||
Source address: 185.1.147.44
|
||||
Hold timer: 174.765/240
|
||||
Keepalive timer: 30.587/80
|
||||
Send hold timer: 425.866/480
|
||||
Channel ipv4
|
||||
State: UP
|
||||
Table: master4
|
||||
Preference: 100
|
||||
Input filter: ebgp_fogixp_47498_import
|
||||
Output filter: ebgp_fogixp_47498_export
|
||||
Receive limit: 50000
|
||||
Action: restart
|
||||
Routes: 44589 imported, 6 filtered, 27 exported, 4173 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 87409 0 17 2 87390
|
||||
Import withdraws: 22781 0 --- 158 22640
|
||||
Export updates: 5998808 25441 5973153 --- 214
|
||||
Export withdraws: 99952 --- --- --- 46
|
||||
BGP Next hop: 185.1.147.44
|
||||
|
||||
fogixp_47498_ipv6_1 BGP --- up 2025-08-20 22:33:06 Established
|
||||
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||
BGP state: Established
|
||||
Neighbor address: 2001:7f8:ca:1::111
|
||||
Neighbor AS: 47498
|
||||
Local AS: 8298
|
||||
Neighbor ID: 185.1.147.111
|
||||
Local capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv6
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Neighbor capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv6
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Session: external AS4
|
||||
Source address: 2001:7f8:ca:1::44
|
||||
Hold timer: 160.527/240
|
||||
Keepalive timer: 22.845/80
|
||||
Send hold timer: 355.702/480
|
||||
Channel ipv6
|
||||
State: UP
|
||||
Table: master6
|
||||
Preference: 100
|
||||
Input filter: ebgp_fogixp_47498_import
|
||||
Output filter: ebgp_fogixp_47498_export
|
||||
Receive limit: 50000
|
||||
Action: restart
|
||||
Routes: 3510 imported, 6 filtered, 47 exported, 668 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 33247408 0 11645 198185 33037578
|
||||
Import withdraws: 10638021 0 --- 162699 10486967
|
||||
Export updates: 648072150 14962764 633024154 --- 85232
|
||||
Export withdraws: 76736402 --- --- --- 19602
|
||||
BGP Next hop: 2001:7f8:ca:1::44 fe80::6a05:caff:fe32:4617
|
||||
|
||||
fogixp_47498_ipv6_2 BGP --- up 2025-08-20 22:33:05 Established
|
||||
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||
BGP state: Established
|
||||
Neighbor address: 2001:7f8:ca:1::222
|
||||
Neighbor AS: 47498
|
||||
Local AS: 8298
|
||||
Neighbor ID: 185.1.147.222
|
||||
Local capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv6
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Neighbor capabilities
|
||||
Multiprotocol
|
||||
AF announced: ipv6
|
||||
Route refresh
|
||||
Graceful restart
|
||||
4-octet AS numbers
|
||||
Enhanced refresh
|
||||
Long-lived graceful restart
|
||||
Session: external AS4
|
||||
Source address: 2001:7f8:ca:1::44
|
||||
Hold timer: 196.907/240
|
||||
Keepalive timer: 2.909/80
|
||||
Send hold timer: 295.173/480
|
||||
Channel ipv6
|
||||
State: UP
|
||||
Table: master6
|
||||
Preference: 100
|
||||
Input filter: ebgp_fogixp_47498_import
|
||||
Output filter: ebgp_fogixp_47498_export
|
||||
Receive limit: 50000
|
||||
Action: restart
|
||||
Routes: 3493 imported, 5 filtered, 47 exported, 2 preferred
|
||||
Route change stats: received rejected filtered ignored accepted
|
||||
Import updates: 34265572 0 10111 198814 34056647
|
||||
Import withdraws: 10602791 0 --- 164049 10448853
|
||||
Export updates: 648071805 7560692 640425882 --- 85231
|
||||
Export withdraws: 76736402 --- --- --- 19602
|
||||
BGP Next hop: 2001:7f8:ca:1::44 fe80::6a05:caff:fe32:4617
|
||||
8
debian/bird-exporter.default
vendored
Normal file
8
debian/bird-exporter.default
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default settings for bird-exporter
|
||||
|
||||
# User and group to run bird-exporter as
|
||||
BIRD_RUN_USER=bird
|
||||
BIRD_RUN_GROUP=bird
|
||||
|
||||
# Command line arguments to pass to bird-exporter
|
||||
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||
24
debian/bird-exporter.service
vendored
Normal file
24
debian/bird-exporter.service
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=BIRD Exporter for Prometheus
|
||||
Documentation=https://git.ipng.ch/ipng/bird-exporter
|
||||
After=network.target bird.service bird6.service
|
||||
Wants=bird.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=-/etc/default/bird-exporter
|
||||
User=${BIRD_RUN_USER}
|
||||
Group=${BIRD_RUN_GROUP}
|
||||
ExecStart=/usr/bin/bird-exporter $BIRD_EXPORTER_ARGS
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/run/bird
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
10
debian/changelog
vendored
Normal file
10
debian/changelog
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
bird-exporter (0.1.0) unstable; urgency=medium
|
||||
|
||||
* Initial release
|
||||
* BIRD socket communication with periodic scraping
|
||||
* Prometheus metrics export for protocols, routes, and BGP
|
||||
* Support for Device, Direct, Kernel, Static, OSPF, and BGP protocols
|
||||
* Route statistics including imported, filtered, exported, and preferred
|
||||
* BGP-specific metrics including timers and session state
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Tue, 31 Dec 2024 14:00:00 +0000
|
||||
27
debian/control
vendored
Normal file
27
debian/control
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
Source: bird-exporter
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
golang-go (>= 1.21)
|
||||
Standards-Version: 4.6.0
|
||||
Homepage: https://git.ipng.ch/ipng/bird-exporter
|
||||
Vcs-Git: https://git.ipng.ch/ipng/bird-exporter.git
|
||||
Vcs-Browser: https://git.ipng.ch/ipng/bird-exporter
|
||||
|
||||
Package: bird-exporter
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Recommends: bird2
|
||||
Description: Prometheus exporter for BIRD routing daemon
|
||||
bird-exporter is a Prometheus metrics exporter for the BIRD routing daemon.
|
||||
It connects to BIRD's control socket and exports metrics about protocols,
|
||||
routes, and BGP sessions.
|
||||
.
|
||||
Features:
|
||||
* Real-time metrics via BIRD control socket
|
||||
* Support for all major BIRD protocol types (BGP, OSPF, Kernel, Static, etc.)
|
||||
* Detailed route statistics (imported, filtered, exported, preferred)
|
||||
* BGP-specific metrics including session state and timers
|
||||
* Configurable scrape interval
|
||||
* Thread-safe periodic updates with caching
|
||||
31
debian/copyright
vendored
Normal file
31
debian/copyright
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: bird-exporter
|
||||
Upstream-Contact: Pim van Pelt <pim@ipng.ch>
|
||||
Source: https://git.ipng.ch/ipng/bird-exporter
|
||||
|
||||
Files: *
|
||||
Copyright: 2024 BIRD Exporter Authors
|
||||
License: MIT
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2024 BIRD Exporter Authors
|
||||
License: MIT
|
||||
|
||||
License: MIT
|
||||
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.
|
||||
1
debian/install
vendored
Normal file
1
debian/install
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bird-exporter usr/bin/
|
||||
33
debian/rules
vendored
Executable file
33
debian/rules
vendored
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
export DH_VERBOSE = 1
|
||||
export GO111MODULE = on
|
||||
export GOPROXY = direct
|
||||
export GOCACHE = $(CURDIR)/debian/.gocache
|
||||
export GOPATH = $(CURDIR)/debian/go
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
go build -v -ldflags="-s -w" -o bird-exporter ./cmd/bird-exporter/
|
||||
|
||||
override_dh_auto_install:
|
||||
install -D -m 0755 bird-exporter debian/bird-exporter/usr/bin/bird-exporter
|
||||
install -D -m 0644 debian/bird-exporter.service debian/bird-exporter/lib/systemd/system/bird-exporter.service
|
||||
install -D -m 0644 debian/bird-exporter.default debian/bird-exporter/etc/default/bird-exporter
|
||||
install -D -m 0644 docs/bird-exporter.1 debian/bird-exporter/usr/share/man/man1/bird-exporter.1
|
||||
|
||||
override_dh_auto_configure:
|
||||
# Skip auto configure
|
||||
|
||||
override_dh_auto_test:
|
||||
go test -v ./cmd/bird-exporter/
|
||||
|
||||
override_dh_auto_clean:
|
||||
rm -f bird-exporter
|
||||
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||
rm -rf debian/.gocache debian/go obj-*
|
||||
|
||||
override_dh_dwz:
|
||||
# Skip dwz compression for Go binaries
|
||||
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@@ -0,0 +1 @@
|
||||
3.0 (native)
|
||||
373
doc/DETAILS.md
Normal file
373
doc/DETAILS.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# BIRD Exporter - Detailed Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
bird-exporter is a Prometheus exporter for the BIRD routing daemon. It parses
|
||||
the output of `birdc show proto all` and exposes metrics in Prometheus format.
|
||||
|
||||
## Architecture
|
||||
|
||||
The exporter consists of three main components:
|
||||
|
||||
1. **Parser** - Parses BIRD protocol output into structured data
|
||||
2. **Collector** - Implements Prometheus collector interface
|
||||
3. **HTTP Server** - Serves metrics on /metrics endpoint
|
||||
|
||||
## Input Format
|
||||
|
||||
The exporter reads output from `birdc show proto all`, which provides detailed
|
||||
information about all configured protocols. Each protocol block contains:
|
||||
|
||||
- Protocol name, type, table, state, and timestamp
|
||||
- Protocol-specific information (BGP state, OSPF status, etc.)
|
||||
- One or more channels (typically ipv4 and/or ipv6)
|
||||
- Route statistics per channel
|
||||
- Import/export statistics per channel
|
||||
|
||||
### Supported Protocol Types
|
||||
|
||||
- **Device** - Interface tracking
|
||||
- **Direct** - Directly connected routes
|
||||
- **Kernel** - Kernel routing table synchronization
|
||||
- **Static** - Static routes
|
||||
- **OSPF** - OSPF routing protocol
|
||||
- **BGP** - Border Gateway Protocol
|
||||
|
||||
## Exported Metrics
|
||||
|
||||
### Protocol Status Metrics
|
||||
|
||||
**bird_protocol_up**
|
||||
- Type: Gauge
|
||||
- Description: Protocol operational status (1=up, 0=down)
|
||||
- Labels: name, proto, table, info
|
||||
|
||||
### BGP-Specific Metrics
|
||||
|
||||
**bird_bgp_state**
|
||||
- Type: Gauge
|
||||
- Description: BGP session state (1=Established, 0=other)
|
||||
- Labels: name, neighbor_address, neighbor_as, local_as, neighbor_id
|
||||
|
||||
**bird_bgp_hold_timer_seconds**
|
||||
- Type: Gauge
|
||||
- Description: Current BGP hold timer value in seconds
|
||||
- Labels: name, neighbor_address
|
||||
|
||||
**bird_bgp_keepalive_timer_seconds**
|
||||
- Type: Gauge
|
||||
- Description: Current BGP keepalive timer value in seconds
|
||||
- Labels: name, neighbor_address
|
||||
|
||||
**bird_bgp_send_hold_timer_seconds**
|
||||
- Type: Gauge
|
||||
- Description: Current BGP send hold timer value in seconds
|
||||
- Labels: name, neighbor_address
|
||||
|
||||
### Route Metrics
|
||||
|
||||
All route metrics include labels: name, proto, channel, table
|
||||
|
||||
**bird_route_imported**
|
||||
- Type: Gauge
|
||||
- Description: Number of routes currently imported
|
||||
|
||||
**bird_route_exported**
|
||||
- Type: Gauge
|
||||
- Description: Number of routes currently exported
|
||||
|
||||
**bird_route_preferred**
|
||||
- Type: Gauge
|
||||
- Description: Number of preferred routes
|
||||
|
||||
### Route Change Statistics
|
||||
|
||||
All statistics are counters with labels: name, proto, channel, table
|
||||
|
||||
**Import Statistics:**
|
||||
- bird_route_import_updates_total
|
||||
- bird_route_import_withdraws_total
|
||||
- bird_route_import_rejected_total
|
||||
- bird_route_import_filtered_total
|
||||
- bird_route_import_ignored_total
|
||||
- bird_route_import_accepted_total
|
||||
|
||||
**Export Statistics:**
|
||||
- bird_route_export_updates_total
|
||||
- bird_route_export_withdraws_total
|
||||
- bird_route_export_rejected_total
|
||||
- bird_route_export_filtered_total
|
||||
- bird_route_export_accepted_total
|
||||
|
||||
## Configuration
|
||||
|
||||
### Command-Line Flags
|
||||
|
||||
**-web.listen-address** (default: `:9324`)
|
||||
The address and port to listen on for HTTP requests.
|
||||
|
||||
Example:
|
||||
```sh
|
||||
./bird-exporter -web.listen-address=:9100
|
||||
```
|
||||
|
||||
**-bird.socket** (default: `/var/run/bird/bird.ctl`)
|
||||
Path to the BIRD control socket for communication with the BIRD daemon.
|
||||
|
||||
Example:
|
||||
```sh
|
||||
./bird-exporter -bird.socket=/run/bird.ctl
|
||||
```
|
||||
|
||||
**-period** (default: `60s`)
|
||||
Time interval between BIRD data scrapes. The exporter caches BIRD data and
|
||||
updates it periodically at this interval.
|
||||
|
||||
Example:
|
||||
```sh
|
||||
./bird-exporter -period=30s
|
||||
```
|
||||
|
||||
**-debug** (default: `false`)
|
||||
Enable debug logging. When enabled, additional log messages are printed including:
|
||||
- "Performing initial scrape..."
|
||||
- "BIRD: show protocols all"
|
||||
- "Successfully scraped N protocols"
|
||||
|
||||
Without debug mode, only essential startup and operational messages are logged.
|
||||
|
||||
Example:
|
||||
```sh
|
||||
./bird-exporter -debug
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.21 or later
|
||||
- Make (optional)
|
||||
|
||||
### Build Commands
|
||||
|
||||
Using Make:
|
||||
```sh
|
||||
make build
|
||||
```
|
||||
|
||||
Using Go directly:
|
||||
```sh
|
||||
go build -o bird-exporter ./cmd/bird-exporter/
|
||||
```
|
||||
|
||||
### Build Output
|
||||
|
||||
The binary will be created in the project root directory:
|
||||
- Binary name: `bird-exporter`
|
||||
- Size: ~12MB (uncompressed)
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
Using Make:
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
Using Go directly:
|
||||
```sh
|
||||
go test -v ./cmd/bird-exporter/
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
The test suite includes:
|
||||
- Protocol parsing tests (Device, Direct, Kernel, Static, OSPF, BGP)
|
||||
- Channel parsing tests
|
||||
- Route statistics parsing tests
|
||||
- BGP-specific field parsing tests
|
||||
- Error handling tests (missing files, empty files)
|
||||
- Integration test with real birdc output
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
Test fixtures are located in `cmd/bird-exporter/testdata/`:
|
||||
- `sample_output.txt` - Minimal test fixture with all protocol types
|
||||
|
||||
## Deployment
|
||||
|
||||
### Running Locally
|
||||
|
||||
For development or testing:
|
||||
```sh
|
||||
./bird-exporter
|
||||
curl http://localhost:9324/metrics
|
||||
```
|
||||
|
||||
### Running in Production
|
||||
|
||||
The exporter communicates with BIRD via its control socket. Ensure the exporter
|
||||
has permission to access the BIRD socket (typically `/var/run/bird/bird.ctl`).
|
||||
|
||||
**Systemd Service**
|
||||
|
||||
Create `/etc/systemd/system/bird-exporter.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=BIRD Exporter
|
||||
After=network.target bird.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bird
|
||||
ExecStart=/usr/local/bin/bird-exporter -period=60s
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
```sh
|
||||
systemctl enable bird-exporter
|
||||
systemctl start bird-exporter
|
||||
```
|
||||
|
||||
### Prometheus Configuration
|
||||
|
||||
Add to prometheus.yml:
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'bird'
|
||||
static_configs:
|
||||
- targets: ['localhost:9324']
|
||||
```
|
||||
|
||||
## Prometheus Queries
|
||||
|
||||
### Example Queries
|
||||
|
||||
Count protocols by state:
|
||||
```promql
|
||||
count by (state) (bird_protocol_up)
|
||||
```
|
||||
|
||||
Count established BGP sessions:
|
||||
```promql
|
||||
sum(bird_bgp_state)
|
||||
```
|
||||
|
||||
Total imported routes by protocol type:
|
||||
```promql
|
||||
sum by (proto) (bird_route_imported)
|
||||
```
|
||||
|
||||
BGP sessions with low hold timer (potential flapping):
|
||||
```promql
|
||||
bird_bgp_hold_timer_seconds < 30
|
||||
```
|
||||
|
||||
Import reject rate:
|
||||
```promql
|
||||
rate(bird_route_import_rejected_total[5m])
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **BIRD v2 Only** - The parser is designed for BIRD v2 output format. BIRD
|
||||
v1 compatibility is not guaranteed.
|
||||
|
||||
2. **Periodic Updates** - Metrics are updated at the configured scrape interval
|
||||
(default 60s). Changes in BIRD state may not be immediately reflected in metrics.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debugging Connection Issues
|
||||
|
||||
Enable debug logging to see detailed information about BIRD communication:
|
||||
```sh
|
||||
./bird-exporter -debug
|
||||
```
|
||||
|
||||
This will show:
|
||||
- When the exporter queries BIRD
|
||||
- What command is being sent
|
||||
- How many protocols were successfully parsed
|
||||
|
||||
### No Metrics Appearing
|
||||
|
||||
Check that the BIRD socket is accessible:
|
||||
```sh
|
||||
ls -la /var/run/bird/bird.ctl
|
||||
```
|
||||
|
||||
Verify BIRD is running:
|
||||
```sh
|
||||
birdc show status
|
||||
```
|
||||
|
||||
Check exporter logs for connection errors:
|
||||
```sh
|
||||
journalctl -u bird-exporter -f
|
||||
```
|
||||
|
||||
### Incorrect Metric Values
|
||||
|
||||
Check exporter logs for parse errors:
|
||||
```sh
|
||||
journalctl -u bird-exporter | grep -i error
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
The exporter loads the entire BIRD output into memory. For routers with many
|
||||
BGP sessions and large routing tables, this can consume significant memory.
|
||||
Monitor memory usage and consider resource limits.
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
bird-exporter/
|
||||
├── cmd/
|
||||
│ └── bird-exporter/
|
||||
│ ├── main.go # Main program
|
||||
│ ├── main_test.go # Tests
|
||||
│ └── testdata/ # Test fixtures
|
||||
├── doc/
|
||||
│ └── DETAILS.md # This file
|
||||
├── go.mod # Go module definition
|
||||
├── Makefile # Build automation
|
||||
└── README.md # Quick start guide
|
||||
```
|
||||
|
||||
### Adding New Metrics
|
||||
|
||||
1. Define metric descriptor in `NewBirdCollector()`
|
||||
2. Add metric to `Describe()` method
|
||||
3. Extract data in parser if needed
|
||||
4. Export metric in `Collect()` method
|
||||
5. Add test coverage
|
||||
|
||||
### Parser Architecture
|
||||
|
||||
The parser uses a state machine approach:
|
||||
- Tracks current protocol being parsed
|
||||
- Tracks current channel within protocol
|
||||
- Uses regex patterns to match and extract fields
|
||||
- Handles hierarchical structure through indentation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Additional Protocols** - Support for RIP, Babel, BFD details, etc.
|
||||
2. **Neighbor Details** - More detailed BGP neighbor information
|
||||
3. **Route Details** - Export information about specific routes
|
||||
4. **Real-time Updates** - Push updates instead of polling
|
||||
5. **Configuration Reload** - Support SIGHUP for config reload
|
||||
6. **Multiple BIRD Instances** - Support monitoring multiple BIRD instances
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
164
docs/bird-exporter.1
Normal file
164
docs/bird-exporter.1
Normal file
@@ -0,0 +1,164 @@
|
||||
.TH BIRD-EXPORTER 1 "December 2024" "bird-exporter 0.1.0" "User Commands"
|
||||
.SH NAME
|
||||
bird-exporter \- Prometheus exporter for BIRD routing daemon
|
||||
.SH SYNOPSIS
|
||||
.B bird-exporter
|
||||
[\fIOPTIONS\fR]
|
||||
.SH DESCRIPTION
|
||||
.B bird-exporter
|
||||
is a Prometheus metrics exporter for the BIRD routing daemon. It connects to
|
||||
BIRD's control socket and exports metrics about protocols, routes, and BGP
|
||||
sessions in a format that can be scraped by Prometheus.
|
||||
.PP
|
||||
The exporter periodically queries BIRD for protocol information and caches the
|
||||
results to minimize load on the BIRD daemon. The cache is updated at a
|
||||
configurable interval (default: 60 seconds) and served to Prometheus on each
|
||||
scrape request.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-web.listen\-address \fIstring\fR
|
||||
Address to listen on for web interface and telemetry (default: ":9324")
|
||||
.TP
|
||||
.B \-bird.socket \fIstring\fR
|
||||
Path to BIRD control socket (default: "/var/run/bird/bird.ctl")
|
||||
.TP
|
||||
.B \-period \fIduration\fR
|
||||
Period between scrapes of BIRD data (default: 60s)
|
||||
.TP
|
||||
.B \-debug
|
||||
Enable debug logging (default: false)
|
||||
.SH METRICS
|
||||
The exporter provides the following metrics:
|
||||
.TP
|
||||
.B bird_protocol_up
|
||||
Protocol status (1 = up, 0 = down). Labels: name, proto, table, info
|
||||
.TP
|
||||
.B bird_bgp_state
|
||||
BGP session state (1 = Established, 0 = other). Labels: name, neighbor_address, neighbor_as, local_as, neighbor_id
|
||||
.TP
|
||||
.B bird_route_imported
|
||||
Number of imported routes. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_filtered
|
||||
Number of filtered routes. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_exported
|
||||
Number of exported routes. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_preferred
|
||||
Number of preferred routes. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_updates_total
|
||||
Total number of import updates. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_withdraws_total
|
||||
Total number of import withdraws. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_rejected_total
|
||||
Total number of rejected imports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_filtered_total
|
||||
Total number of filtered imports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_ignored_total
|
||||
Total number of ignored imports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_import_accepted_total
|
||||
Total number of accepted imports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_export_updates_total
|
||||
Total number of export updates. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_export_withdraws_total
|
||||
Total number of export withdraws. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_export_rejected_total
|
||||
Total number of rejected exports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_export_filtered_total
|
||||
Total number of filtered exports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_route_export_accepted_total
|
||||
Total number of accepted exports. Labels: name, proto, channel, table
|
||||
.TP
|
||||
.B bird_bgp_hold_timer_seconds
|
||||
BGP hold timer in seconds. Labels: name, neighbor_address
|
||||
.TP
|
||||
.B bird_bgp_keepalive_timer_seconds
|
||||
BGP keepalive timer in seconds. Labels: name, neighbor_address
|
||||
.TP
|
||||
.B bird_bgp_send_hold_timer_seconds
|
||||
BGP send hold timer in seconds. Labels: name, neighbor_address
|
||||
.SH CONFIGURATION
|
||||
Default configuration can be set in \fB/etc/default/bird-exporter\fR. The file
|
||||
should contain a BIRD_EXPORTER_ARGS variable with command line arguments.
|
||||
.PP
|
||||
Example:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||
.RE
|
||||
.fi
|
||||
.SH EXAMPLES
|
||||
Start the exporter with default settings:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
bird-exporter
|
||||
.RE
|
||||
.fi
|
||||
.PP
|
||||
Start with custom BIRD socket path:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
bird-exporter -bird.socket=/run/bird.ctl
|
||||
.RE
|
||||
.fi
|
||||
.PP
|
||||
Start with custom scrape interval and listen address:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
bird-exporter -period=30s -web.listen-address=:9999
|
||||
.RE
|
||||
.fi
|
||||
.PP
|
||||
Start with debug logging enabled:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
bird-exporter -debug
|
||||
.RE
|
||||
.fi
|
||||
.PP
|
||||
Query metrics using curl:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
curl http://localhost:9324/metrics
|
||||
.RE
|
||||
.fi
|
||||
.SH FILES
|
||||
.TP
|
||||
.B /etc/default/bird-exporter
|
||||
Default configuration file
|
||||
.TP
|
||||
.B /var/run/bird/bird.ctl
|
||||
Default BIRD control socket path
|
||||
.TP
|
||||
.B /lib/systemd/system/bird-exporter.service
|
||||
Systemd service unit file
|
||||
.SH PERMISSIONS
|
||||
The exporter needs read/write access to the BIRD control socket. When running
|
||||
as a systemd service, it runs as the \fBbird\fR user which should have
|
||||
appropriate permissions.
|
||||
.SH SEE ALSO
|
||||
.BR bird (1),
|
||||
.BR birdc (1),
|
||||
.BR prometheus (1)
|
||||
.PP
|
||||
Project homepage: https://git.ipng.ch/ipng/bird-exporter
|
||||
.SH AUTHOR
|
||||
BIRD Exporter Authors
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module github.com/pim/bird-exporter
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/prometheus/client_golang v1.19.0
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
||||
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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
Reference in New Issue
Block a user