From 3b22822d9283cb04e63df63293f910c29fa6350b Mon Sep 17 00:00:00 2001 From: Daniel Swarbrick Date: Sat, 17 Feb 2024 02:34:25 +0100 Subject: [PATCH] Parse request / response size histograms (via XMLv3 stats) This implements parsing of the DNS request / response traffic size histograms, for the JSON statistics channel. Refs: #64 Signed-off-by: Daniel Swarbrick --- bind/json/json.go | 20 ++++---- bind/xml/xml.go | 108 ++++++++++++++++++++++++++++++++++++++- bind_exporter_test.go | 25 +++++---- fixtures/xml/traffic.xml | 103 +++++++++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 22 deletions(-) create mode 100644 fixtures/xml/traffic.xml diff --git a/bind/json/json.go b/bind/json/json.go index 0538e04..e65048e 100644 --- a/bind/json/json.go +++ b/bind/json/json.go @@ -78,7 +78,7 @@ type TrafficStatistics struct { Traffic struct { ReceivedUDPv4 map[string]uint64 `json:"dns-udp-requests-sizes-received-ipv4"` SentUDPv4 map[string]uint64 `json:"dns-udp-responses-sizes-sent-ipv4"` - ReceivedTCPv4 map[string]uint64 `json:"dns-tcp-requests-sizes-sent-ipv4"` + ReceivedTCPv4 map[string]uint64 `json:"dns-tcp-requests-sizes-received-ipv4"` SentTCPv4 map[string]uint64 `json:"dns-tcp-responses-sizes-sent-ipv4"` ReceivedUDPv6 map[string]uint64 `json:"dns-udp-requests-sizes-received-ipv6"` SentUDPv6 map[string]uint64 `json:"dns-udp-responses-sizes-sent-ipv6"` @@ -216,30 +216,30 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) { var err error // Make IPv4 traffic histograms. - if s.TrafficHistograms.ReceivedUDPv4, err = parseTrafficHist(trafficStats.Traffic.ReceivedUDPv4, bind.TrafficInMaxSize); err != nil { + if s.TrafficHistograms.ReceivedUDPv4, err = processTrafficCounters(trafficStats.Traffic.ReceivedUDPv4, bind.TrafficInMaxSize); err != nil { return s, err } - if s.TrafficHistograms.SentUDPv4, err = parseTrafficHist(trafficStats.Traffic.SentUDPv4, bind.TrafficOutMaxSize); err != nil { + if s.TrafficHistograms.SentUDPv4, err = processTrafficCounters(trafficStats.Traffic.SentUDPv4, bind.TrafficOutMaxSize); err != nil { return s, err } - if s.TrafficHistograms.ReceivedTCPv4, err = parseTrafficHist(trafficStats.Traffic.ReceivedTCPv4, bind.TrafficInMaxSize); err != nil { + if s.TrafficHistograms.ReceivedTCPv4, err = processTrafficCounters(trafficStats.Traffic.ReceivedTCPv4, bind.TrafficInMaxSize); err != nil { return s, err } - if s.TrafficHistograms.SentTCPv4, err = parseTrafficHist(trafficStats.Traffic.SentTCPv4, bind.TrafficOutMaxSize); err != nil { + if s.TrafficHistograms.SentTCPv4, err = processTrafficCounters(trafficStats.Traffic.SentTCPv4, bind.TrafficOutMaxSize); err != nil { return s, err } // Make IPv6 traffic histograms. - if s.TrafficHistograms.ReceivedUDPv6, err = parseTrafficHist(trafficStats.Traffic.ReceivedUDPv6, bind.TrafficInMaxSize); err != nil { + if s.TrafficHistograms.ReceivedUDPv6, err = processTrafficCounters(trafficStats.Traffic.ReceivedUDPv6, bind.TrafficInMaxSize); err != nil { return s, err } - if s.TrafficHistograms.SentUDPv6, err = parseTrafficHist(trafficStats.Traffic.SentUDPv6, bind.TrafficOutMaxSize); err != nil { + if s.TrafficHistograms.SentUDPv6, err = processTrafficCounters(trafficStats.Traffic.SentUDPv6, bind.TrafficOutMaxSize); err != nil { return s, err } - if s.TrafficHistograms.ReceivedTCPv6, err = parseTrafficHist(trafficStats.Traffic.ReceivedTCPv6, bind.TrafficInMaxSize); err != nil { + if s.TrafficHistograms.ReceivedTCPv6, err = processTrafficCounters(trafficStats.Traffic.ReceivedTCPv6, bind.TrafficInMaxSize); err != nil { return s, err } - if s.TrafficHistograms.SentTCPv6, err = parseTrafficHist(trafficStats.Traffic.SentTCPv6, bind.TrafficOutMaxSize); err != nil { + if s.TrafficHistograms.SentTCPv6, err = processTrafficCounters(trafficStats.Traffic.SentTCPv6, bind.TrafficOutMaxSize); err != nil { return s, err } } @@ -247,7 +247,7 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) { return s, nil } -func parseTrafficHist(traffic map[string]uint64, maxBucket uint) ([]uint64, error) { +func processTrafficCounters(traffic map[string]uint64, maxBucket uint) ([]uint64, error) { trafficHist := make([]uint64, maxBucket/bind.TrafficBucketSize) for k, v := range traffic { diff --git a/bind/xml/xml.go b/bind/xml/xml.go index cfa357c..c4e5f8d 100644 --- a/bind/xml/xml.go +++ b/bind/xml/xml.go @@ -19,6 +19,8 @@ import ( "net/http" "net/url" "path" + "strconv" + "strings" "time" "github.com/prometheus-community/bind_exporter/bind" @@ -31,6 +33,8 @@ const ( StatusPath = "/xml/v3/status" // TasksPath is the HTTP path of the v3 tasks resource. TasksPath = "/xml/v3/tasks" + // TrafficPath is the HTTP path of the v3 traffic resource. + TrafficPath = "/xml/v3/traffic" // ZonesPath is the HTTP path of the v3 zones resource. ZonesPath = "/xml/v3/zones" @@ -86,6 +90,13 @@ type ZoneCounter struct { Serial string `xml:"serial"` } +type TrafficStatistics struct { + UDPv4 []Counters `xml:"traffic>ipv4>udp>counters"` + TCPv4 []Counters `xml:"traffic>ipv4>tcp>counters"` + UDPv6 []Counters `xml:"traffic>ipv6>udp>counters"` + TCPv6 []Counters `xml:"traffic>ipv6>tcp>counters"` +} + // Client implements bind.Client and can be used to query a BIND XML v3 API. type Client struct { url string @@ -136,7 +147,6 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) { } var stats Statistics - var zonestats ZoneStatistics if m[bind.ServerStats] || m[bind.ViewStats] { if err := c.Get(ServerPath, &stats); err != nil { return s, err @@ -176,6 +186,7 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) { } } + var zonestats ZoneStatistics if err := c.Get(ZonesPath, &zonestats); err != nil { return s, err } @@ -204,5 +215,100 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) { s.TaskManager = stats.Taskmgr } + if m[bind.TrafficStats] { + var trafficStats TrafficStatistics + if err := c.Get(TrafficPath, &trafficStats); err != nil { + return s, err + } + + var err error + + // Make IPv4 traffic histograms. + for _, cGroup := range trafficStats.UDPv4 { + switch cGroup.Type { + case "request-size": + if s.TrafficHistograms.ReceivedUDPv4, err = processTrafficCounters(cGroup.Counters, bind.TrafficInMaxSize); err != nil { + return s, err + } + case "response-size": + if s.TrafficHistograms.SentUDPv4, err = processTrafficCounters(cGroup.Counters, bind.TrafficOutMaxSize); err != nil { + return s, err + } + } + } + for _, cGroup := range trafficStats.TCPv4 { + switch cGroup.Type { + case "request-size": + if s.TrafficHistograms.ReceivedTCPv4, err = processTrafficCounters(cGroup.Counters, bind.TrafficInMaxSize); err != nil { + return s, err + } + case "response-size": + if s.TrafficHistograms.SentTCPv4, err = processTrafficCounters(cGroup.Counters, bind.TrafficOutMaxSize); err != nil { + return s, err + } + } + } + + // Make IPv6 traffic histograms. + for _, cGroup := range trafficStats.UDPv6 { + switch cGroup.Type { + case "request-size": + if s.TrafficHistograms.ReceivedUDPv6, err = processTrafficCounters(cGroup.Counters, bind.TrafficInMaxSize); err != nil { + return s, err + } + case "response-size": + if s.TrafficHistograms.SentUDPv6, err = processTrafficCounters(cGroup.Counters, bind.TrafficOutMaxSize); err != nil { + return s, err + } + } + } + for _, cGroup := range trafficStats.TCPv6 { + switch cGroup.Type { + case "request-size": + if s.TrafficHistograms.ReceivedTCPv6, err = processTrafficCounters(cGroup.Counters, bind.TrafficInMaxSize); err != nil { + return s, err + } + case "response-size": + if s.TrafficHistograms.SentTCPv6, err = processTrafficCounters(cGroup.Counters, bind.TrafficOutMaxSize); err != nil { + return s, err + } + } + } + } + return s, nil } + +func processTrafficCounters(traffic []bind.Counter, maxBucket uint) ([]uint64, error) { + trafficHist := make([]uint64, maxBucket/bind.TrafficBucketSize) + + for _, c := range traffic { + // Keys are in the format "lowerBound-upperBound". We are only interested in the upper + // bound. + parts := strings.Split(c.Name, "-") + if len(parts) != 2 { + return nil, fmt.Errorf("malformed traffic bucket range: %q", c.Name) + } + + upperBound, err := strconv.ParseUint(parts[1], 10, 16) + if err != nil { + return nil, fmt.Errorf("cannot convert bucket upper bound to uint: %w", err) + } + + if (upperBound+1)%bind.TrafficBucketSize != 0 { + return nil, fmt.Errorf("upper bucket bound is not a multiple of %d minus one: %d", + bind.TrafficBucketSize, upperBound) + } + + if upperBound < uint64(maxBucket) { + // idx is offset, since there is no 0-16 bucket reported by BIND. + idx := (upperBound+1)/bind.TrafficBucketSize - 2 + trafficHist[idx] += c.Counter + } else { + // Final slice element aggregates packet sizes from maxBucket to +Inf. + trafficHist[len(trafficHist)-1] += c.Counter + } + } + + return trafficHist, nil +} diff --git a/bind_exporter_test.go b/bind_exporter_test.go index e8fe67f..9811dab 100644 --- a/bind_exporter_test.go +++ b/bind_exporter_test.go @@ -71,9 +71,14 @@ var ( `bind_worker_threads 16`, } trafficStats = []string{ - `bind_traffic_received_size_bucket{transport="tcpv4",le="+Inf"} 0`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="31"} 18600`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="47"} 111799`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="63"} 134091`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="79"} 134798`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="95"} 134801`, + `bind_traffic_received_size_bucket{transport="tcpv4",le="+Inf"} 134801`, `bind_traffic_received_size_sum{transport="tcpv4"} NaN`, - `bind_traffic_received_size_count{transport="tcpv4"} 0`, + `bind_traffic_received_size_count{transport="tcpv4"} 134801`, `bind_traffic_received_size_bucket{transport="udpv4",le="31"} 9992`, `bind_traffic_received_size_bucket{transport="udpv4",le="47"} 82206`, `bind_traffic_received_size_bucket{transport="udpv4",le="63"} 108619`, @@ -144,9 +149,6 @@ var ( `bind_traffic_sent_size_bucket{transport="udpv4",le="+Inf"} 97951`, `bind_traffic_sent_size_sum{transport="udpv4"} NaN`, `bind_traffic_sent_size_count{transport="udpv4"} 97951`, - `bind_traffic_sent_size_bucket{transport="udpv6",le="+Inf"} 0`, - `bind_traffic_sent_size_sum{transport="udpv6"} NaN`, - `bind_traffic_sent_size_count{transport="udpv6"} 0`, } ) @@ -162,9 +164,9 @@ func TestBindExporterJSONClient(t *testing.T) { func TestBindExporterV3Client(t *testing.T) { bindExporterTest{ server: newV3Server(), - groups: []bind.StatisticGroup{bind.ServerStats, bind.ViewStats, bind.TaskStats}, + groups: []bind.StatisticGroup{bind.ServerStats, bind.ViewStats, bind.TaskStats, bind.TrafficStats}, version: "xml.v3", - include: combine([]string{`bind_up 1`}, serverStats, viewStats, taskStats), + include: combine([]string{`bind_up 1`}, serverStats, viewStats, taskStats, trafficStats), }.run(t) } @@ -234,10 +236,11 @@ func collect(c prometheus.Collector) ([]byte, error) { func newV3Server() *httptest.Server { m := map[string]string{ - "/xml/v3/server": "fixtures/xml/server.xml", - "/xml/v3/status": "fixtures/xml/status.xml", - "/xml/v3/tasks": "fixtures/xml/tasks.xml", - "/xml/v3/zones": "fixtures/xml/zones.xml", + "/xml/v3/server": "fixtures/xml/server.xml", + "/xml/v3/status": "fixtures/xml/status.xml", + "/xml/v3/tasks": "fixtures/xml/tasks.xml", + "/xml/v3/traffic": "fixtures/xml/traffic.xml", + "/xml/v3/zones": "fixtures/xml/zones.xml", } return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if f, ok := m[r.RequestURI]; ok { diff --git a/fixtures/xml/traffic.xml b/fixtures/xml/traffic.xml new file mode 100644 index 0000000..a611c87 --- /dev/null +++ b/fixtures/xml/traffic.xml @@ -0,0 +1,103 @@ + + + + + 2024-02-04T17:26:28.754Z + 2024-02-04T17:26:28.846Z + 2024-02-16T23:35:08.782Z + 9.19.19-1-Debian + + + + + + 9992 + 72214 + 26413 + 1011 + 53 + 5 + 3 + + + 11 + 655 + 9372 + 8509 + 15889 + 18710 + 6369 + 7446 + 5097 + 6053 + 4761 + 3082 + 3275 + 4232 + 1326 + 1439 + 353 + 441 + 633 + 32 + 16 + 16 + 76 + 27 + 40 + 39 + 19 + 33 + + + + + 18600 + 93199 + 22292 + 707 + 3 + + + 28 + 203 + 16502 + 13754 + 19264 + 13504 + 8981 + 14979 + 7757 + 9189 + 7643 + 6973 + 2783 + 5374 + 2496 + 1731 + 462 + 512 + 1077 + 953 + 11 + 309 + 106 + 80 + 9 + 121 + + + + + + + + + + + + + + + +