Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user