bird_exporter/parser/parser.go
2022-01-27 12:20:22 +01:00

274 lines
5.5 KiB
Go

package parser
import (
"bufio"
"bytes"
"regexp"
"strconv"
"strings"
"github.com/czerwonk/bird_exporter/protocol"
)
var (
protocolRegex *regexp.Regexp
descriptionRegex *regexp.Regexp
routeRegex *regexp.Regexp
uptimeRegex *regexp.Regexp
routeChangeRegex *regexp.Regexp
filterRegex *regexp.Regexp
channelRegex *regexp.Regexp
)
type context struct {
current *protocol.Protocol
line string
handled bool
protocols []*protocol.Protocol
ipVersion string
}
func init() {
protocolRegex = regexp.MustCompile(`^(?:1002\-)?([^\s]+)\s+(MRT|BGP|BFD|OSPF|RPKI|RIP|RAdv|Pipe|Perf|Direct|Babel|Device|Kernel|Static)\s+([^\s]+)\s+([^\s]+)\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|[^\s]+)(?:\s+(.*?))?$`)
descriptionRegex = regexp.MustCompile(`Description:\s+(.*)`)
routeRegex = regexp.MustCompile(`^\s+Routes:\s+(\d+) imported, (?:(\d+) filtered, )?(\d+) exported(?:, (\d+) preferred)?`)
uptimeRegex = regexp.MustCompile(`^(?:((\d+):(\d{2}):(\d{2}))|(\d+)|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}))$`)
routeChangeRegex = regexp.MustCompile(`(Import|Export) (updates|withdraws):\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s*`)
filterRegex = regexp.MustCompile(`(Input|Output) filter:\s+(.*)`)
channelRegex = regexp.MustCompile(`Channel ipv(4|6)`)
}
// ParseProtocols parses bird output and returns protocol.Protocol structs
func ParseProtocols(data []byte, ipVersion string) []*protocol.Protocol {
reader := bytes.NewReader(data)
scanner := bufio.NewScanner(reader)
c := &context{protocols: make([]*protocol.Protocol, 0), ipVersion: ipVersion}
var handlers = []func(*context){
handleEmptyLine,
parseLineForProtocol,
parseLineForDescription,
parseLineForChannel,
parseLineForRoutes,
parseLineForRouteChanges,
parseLineForFilterName,
}
for scanner.Scan() {
c.line = strings.TrimRight(scanner.Text(), " ")
c.handled = false
for _, h := range handlers {
if !c.handled {
h(c)
}
}
}
return c.protocols
}
func handleEmptyLine(c *context) {
if c.line != "" {
return
}
c.current = nil
c.handled = true
}
func parseLineForProtocol(c *context) {
match := protocolRegex.FindStringSubmatch(c.line)
if match == nil {
return
}
proto := parseProto(match[2])
ut := parseUptime(match[5])
c.current = protocol.NewProtocol(match[1], proto, c.ipVersion, ut)
c.current.Up = parseState(match[4])
c.current.State = match[6]
c.protocols = append(c.protocols, c.current)
c.handled = true
}
func parseLineForDescription(c *context) {
match := descriptionRegex.FindStringSubmatch(c.line)
if match == nil {
return
}
if len(match) <= 1 {
return
}
c.current.Description = strings.Join(match[1:], " ")
}
func parseProto(val string) protocol.Proto {
switch val {
case "BGP":
return protocol.BGP
case "OSPF":
return protocol.OSPF
case "Direct":
return protocol.Direct
case "Kernel":
return protocol.Kernel
case "Static":
return protocol.Static
case "Babel":
return protocol.Babel
case "RPKI":
return protocol.RPKI
case "BFD":
return protocol.BFD
}
return protocol.PROTO_UNKNOWN
}
func parseState(state string) int {
if state == "up" {
return 1
}
return 0
}
func parseUptime(value string) int {
match := uptimeRegex.FindStringSubmatch(value)
if match == nil {
return 0
}
if len(match[1]) > 0 {
return parseUptimeForDuration(match)
}
if len(match[5]) > 0 {
return parseUptimeForTimestamp(value)
}
return parseUptimeForIso(value)
}
func parseLineForChannel(c *context) {
if c.ipVersion != "" || c.current == nil {
return
}
channel := channelRegex.FindStringSubmatch(c.line)
if channel == nil {
return
}
if len(c.current.IPVersion) == 0 {
c.current.IPVersion = channel[1]
} else {
c.current = &protocol.Protocol{
Name: c.current.Name,
Proto: c.current.Proto,
Up: c.current.Up,
Uptime: c.current.Uptime,
IPVersion: channel[1],
}
c.protocols = append(c.protocols, c.current)
}
c.handled = true
}
func parseLineForRoutes(c *context) {
if c.current == nil {
return
}
match := routeRegex.FindStringSubmatch(c.line)
if match == nil {
return
}
c.current.Imported, _ = strconv.ParseInt(match[1], 10, 64)
c.current.Exported, _ = strconv.ParseInt(match[3], 10, 64)
if len(match[2]) > 0 {
c.current.Filtered, _ = strconv.ParseInt(match[2], 10, 64)
}
if len(match[4]) > 0 {
c.current.Preferred, _ = strconv.ParseInt(match[4], 10, 64)
}
c.handled = true
}
func parseLineForRouteChanges(c *context) {
if c.current == nil {
return
}
match := routeChangeRegex.FindStringSubmatch(c.line)
if match == nil {
return
}
x := getRouteChangeCount(match, c.current)
x.Received = parseRouteChangeValue(match[3])
x.Rejected = parseRouteChangeValue(match[4])
x.Filtered = parseRouteChangeValue(match[5])
x.Ignored = parseRouteChangeValue(match[6])
x.Accepted = parseRouteChangeValue(match[7])
c.handled = true
}
func getRouteChangeCount(values []string, p *protocol.Protocol) *protocol.RouteChangeCount {
if values[1] == "Import" {
if values[2] == "updates" {
return &p.ImportUpdates
}
return &p.ImportWithdraws
}
if values[2] == "updates" {
return &p.ExportUpdates
}
return &p.ExportWithdraws
}
func parseRouteChangeValue(value string) int64 {
if value == "---" {
return 0
}
return parseInt(value)
}
func parseLineForFilterName(c *context) {
if c.current == nil {
return
}
match := filterRegex.FindStringSubmatch(c.line)
if match == nil {
return
}
if match[1] == "Input" {
c.current.ImportFilter = match[2]
} else {
c.current.ExportFilter = match[2]
}
c.handled = true
}