Initial commit

This commit is contained in:
2025-12-31 15:36:54 +01:00
commit f95e0edd32
19 changed files with 2432 additions and 0 deletions

10
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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))
}

View 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())
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
bird-exporter usr/bin/

33
debian/rules vendored Executable file
View 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
View File

@@ -0,0 +1 @@
3.0 (native)

373
doc/DETAILS.md Normal file
View 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
View 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
View 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
View 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=