package stats import ( "net/netip" "regexp" "strings" "time" ) // Event is one response named logged. type Event struct { Time time.Time Client netip.Addr View string // "" without views Name string // lowercase, without the final dot ("." for the root) Type string // e.g. A, AAAA, PTR RCode string // e.g. NOERROR, NXDOMAIN } // responseRe matches named's response log lines (BIND 9.20, print-time): // // 07-Oct-2026 19:46:22.195 client @0x7135... 10.10.0.37#51709 (nas.adbraun.lol): view internal: response: nas.adbraun.lol IN A NOERROR 1 0 1 +E(0)K (10.10.0.2) // // The "view NAME: " part is missing when no views are configured. var responseRe = regexp.MustCompile(`^(\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2}\.\d{3}) client @0x[0-9a-fA-F]+ (\S+)#\d+ \(.*?\): (?:view (.+?): )?response: (\S+) (\S+) (\S+) ([A-Z0-9]+) `) const logTimeLayout = "02-Jan-2006 15:04:05.000" // ParseResponse parses a response log line. ok is false for other lines // (named also logs to the channel when it starts and rotates files). func ParseResponse(line string, loc *time.Location) (ev Event, ok bool) { m := responseRe.FindStringSubmatch(line) if m == nil { return ev, false } t, err := time.ParseInLocation(logTimeLayout, m[1], loc) if err != nil { return ev, false } client, err := netip.ParseAddr(m[2]) if err != nil { return ev, false } return Event{ Time: t, Client: client.Unmap(), View: m[3], Name: normalizeName(m[4]), Type: m[6], RCode: m[7], }, true } func normalizeName(n string) string { n = strings.ToLower(n) if n == "." { return n } return strings.TrimSuffix(n, ".") }