<commit_before>package parser

import (
	"bufio"
	"bytes"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/czerwonk/bird_exporter/protocol"
	"github.com/prometheus/common/log"
)

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])

	fillAttributes(c.current, match)

	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) int {
	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
	}

	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 parseUptimeForIso(s string) int {
	start, err := time.Parse("2006-01-02 15:04:05", s)
	if err != nil {
		log.Errorln(err)
		return 0
	}

	return int(time.Since(start).Seconds())
}

func parseUptimeForDuration(duration []string) int {
	h := parseInt(duration[2])
	m := parseInt(duration[3])
	s := parseInt(duration[4])
	str := fmt.Sprintf("%dh%dm%ds", h, m, s)

	d, err := time.ParseDuration(str)
	if err != nil {
		log.Errorln(err)
		return 0
	}<commit_msg>Parse uptime with timezone info<commit_after>package parser

import (
	"bufio"
	"bytes"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/czerwonk/bird_exporter/protocol"
	"github.com/prometheus/common/log"
)

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])

	fillAttributes(c.current, match)

	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) int {
	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
	}

	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 parseUptimeForIso(s string) int {
	start, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
	if err != nil {
		log.Errorln(err)
		return 0
	}

	return int(time.Since(start).Seconds())
}

func parseUptimeForDuration(duration []string) int {
	h := parseInt(duration[2])
	m := parseInt(duration[3])
	s := parseInt(duration[4])
	str := fmt.Sprintf("%dh%dm%ds", h, m, s)

	d, err := time.ParseDuration(str)
	if err != nil {
		log.Errorln(err)
		return 0
	}
{'fraction_non_alphanumeric': 0.18021921998470558, 'fraction_numerical': 0.013510068824878919, 'mean_word_length': 4.305611899932387, 'pattern_counts': {'":': 10, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '300401', 'n_tokens_mistral': 3182, 'n_tokens_neox': 2987, 'n_words': 549}