Skip to content

Commit 861d8b0

Browse files
committed
Parse request / response size histograms (via JSON stats)
This implements parsing of the DNS request / response traffic size histograms, for the JSON statistics channel. Refs: #64 Signed-off-by: Daniel Swarbrick <[email protected]>
1 parent 0f6d22f commit 861d8b0

File tree

3 files changed

+222
-12
lines changed

3 files changed

+222
-12
lines changed

Diff for: bind/bind.go

+34-7
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,38 @@ type Client interface {
2626
const (
2727
// QryRTT is the common prefix of query round-trip histogram counters.
2828
QryRTT = "QryRTT"
29+
30+
// trafficBucketSize is the size of one traffic histogram bucket, defined as
31+
// DNS_SIZEHISTO_QUANTUM in BIND source code.
32+
TrafficBucketSize = 16
33+
34+
// trafficInMaxSize is the maximum size inbound request reported by BIND, referred to by
35+
// DNS_SIZEHISTO_MAXIN in BIND source code.
36+
TrafficInMaxSize = 288
37+
38+
// trafficOutMaxSize is the maximum size outbound response reported by BIND, referred to by
39+
// DNS_SIZEHISTO_MAXOUT in BIND source code.
40+
TrafficOutMaxSize = 4096
2941
)
3042

3143
// StatisticGroup describes a sub-group of BIND statistics.
3244
type StatisticGroup string
3345

3446
// Available statistic groups.
3547
const (
36-
ServerStats StatisticGroup = "server"
37-
ViewStats StatisticGroup = "view"
38-
TaskStats StatisticGroup = "tasks"
48+
ServerStats StatisticGroup = "server"
49+
TaskStats StatisticGroup = "tasks"
50+
TrafficStats StatisticGroup = "traffic"
51+
ViewStats StatisticGroup = "view"
3952
)
4053

4154
// Statistics is a generic representation of BIND statistics.
4255
type Statistics struct {
43-
Server Server
44-
Views []View
45-
ZoneViews []ZoneView
46-
TaskManager TaskManager
56+
Server Server
57+
Views []View
58+
ZoneViews []ZoneView
59+
TaskManager TaskManager
60+
TrafficHistograms TrafficHistograms
4761
}
4862

4963
// Server represents BIND server statistics.
@@ -111,3 +125,16 @@ type ThreadModel struct {
111125
DefaultQuantum uint64 `xml:"default-quantum"`
112126
TasksRunning uint64 `xml:"tasks-running"`
113127
}
128+
129+
// TrafficHistograms contains slices representing sent / received traffic, with each slice element
130+
// corresponding to a `TrafficBucketSize` range. The last slice element represents the +Inf bucket.
131+
type TrafficHistograms struct {
132+
ReceivedUDPv4 []uint64
133+
SentUDPv4 []uint64
134+
ReceivedTCPv4 []uint64
135+
SentTCPv4 []uint64
136+
ReceivedUDPv6 []uint64
137+
SentUDPv6 []uint64
138+
ReceivedTCPv6 []uint64
139+
SentTCPv6 []uint64
140+
}

Diff for: bind/json/json.go

+87
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"net/url"
2121
"path"
2222
"strconv"
23+
"strings"
2324
"time"
2425

2526
"github.com/prometheus-community/bind_exporter/bind"
@@ -30,6 +31,8 @@ const (
3031
ServerPath = "/json/v1/server"
3132
// TasksPath is the HTTP path of the JSON v1 tasks resource.
3233
TasksPath = "/json/v1/tasks"
34+
// TrafficPath is the HTTP path of the JSON v1 traffic resource.
35+
TrafficPath = "/json/v1/traffic"
3336
// ZonesPath is the HTTP path of the JSON v1 zones resource.
3437
ZonesPath = "/json/v1/zones"
3538
)
@@ -71,6 +74,19 @@ type TaskStatistics struct {
7174
} `json:"taskmgr"`
7275
}
7376

77+
type TrafficStatistics struct {
78+
Traffic struct {
79+
ReceivedUDPv4 map[string]uint64 `json:"dns-udp-requests-sizes-received-ipv4"`
80+
SentUDPv4 map[string]uint64 `json:"dns-udp-responses-sizes-sent-ipv4"`
81+
ReceivedTCPv4 map[string]uint64 `json:"dns-tcp-requests-sizes-sent-ipv4"`
82+
SentTCPv4 map[string]uint64 `json:"dns-tcp-responses-sizes-sent-ipv4"`
83+
ReceivedUDPv6 map[string]uint64 `json:"dns-udp-requests-sizes-received-ipv6"`
84+
SentUDPv6 map[string]uint64 `json:"dns-udp-responses-sizes-sent-ipv6"`
85+
ReceivedTCPv6 map[string]uint64 `json:"dns-tcp-requests-sizes-sent-ipv6"`
86+
SentTCPv6 map[string]uint64 `json:"dns-tcp-responses-sizes-sent-ipv6"`
87+
} `json:"traffic"`
88+
}
89+
7490
// Client implements bind.Client and can be used to query a BIND JSON v1 API.
7591
type Client struct {
7692
url string
@@ -191,5 +207,76 @@ func (c *Client) Stats(groups ...bind.StatisticGroup) (bind.Statistics, error) {
191207
s.TaskManager.ThreadModel.WorkerThreads = taskstats.TaskMgr.WorkerThreads
192208
}
193209

210+
if m[bind.TrafficStats] {
211+
var trafficStats TrafficStatistics
212+
if err := c.Get(TrafficPath, &trafficStats); err != nil {
213+
return s, err
214+
}
215+
216+
var err error
217+
218+
// Make IPv4 traffic histograms.
219+
if s.TrafficHistograms.ReceivedUDPv4, err = parseTrafficHist(trafficStats.Traffic.ReceivedUDPv4, bind.TrafficInMaxSize); err != nil {
220+
return s, err
221+
}
222+
if s.TrafficHistograms.SentUDPv4, err = parseTrafficHist(trafficStats.Traffic.SentUDPv4, bind.TrafficOutMaxSize); err != nil {
223+
return s, err
224+
}
225+
if s.TrafficHistograms.ReceivedTCPv4, err = parseTrafficHist(trafficStats.Traffic.ReceivedTCPv4, bind.TrafficInMaxSize); err != nil {
226+
return s, err
227+
}
228+
if s.TrafficHistograms.SentTCPv4, err = parseTrafficHist(trafficStats.Traffic.SentTCPv4, bind.TrafficOutMaxSize); err != nil {
229+
return s, err
230+
}
231+
232+
// Make IPv6 traffic histograms.
233+
if s.TrafficHistograms.ReceivedUDPv6, err = parseTrafficHist(trafficStats.Traffic.ReceivedUDPv6, bind.TrafficInMaxSize); err != nil {
234+
return s, err
235+
}
236+
if s.TrafficHistograms.SentUDPv6, err = parseTrafficHist(trafficStats.Traffic.SentUDPv6, bind.TrafficOutMaxSize); err != nil {
237+
return s, err
238+
}
239+
if s.TrafficHistograms.ReceivedTCPv6, err = parseTrafficHist(trafficStats.Traffic.ReceivedTCPv6, bind.TrafficInMaxSize); err != nil {
240+
return s, err
241+
}
242+
if s.TrafficHistograms.SentTCPv6, err = parseTrafficHist(trafficStats.Traffic.SentTCPv6, bind.TrafficOutMaxSize); err != nil {
243+
return s, err
244+
}
245+
}
246+
194247
return s, nil
195248
}
249+
250+
func parseTrafficHist(traffic map[string]uint64, maxBucket uint) ([]uint64, error) {
251+
trafficHist := make([]uint64, maxBucket/bind.TrafficBucketSize)
252+
253+
for k, v := range traffic {
254+
// Keys are in the format "lowerBound-upperBound". We are only interested in the upper
255+
// bound.
256+
parts := strings.Split(k, "-")
257+
if len(parts) != 2 {
258+
return nil, fmt.Errorf("malformed traffic bucket range: %q", k)
259+
}
260+
261+
upperBound, err := strconv.ParseUint(parts[1], 10, 16)
262+
if err != nil {
263+
return nil, fmt.Errorf("cannot convert bucket upper bound to uint: %w", err)
264+
}
265+
266+
if (upperBound+1)%bind.TrafficBucketSize != 0 {
267+
return nil, fmt.Errorf("upper bucket bound is not a multiple of %d minus one: %d",
268+
bind.TrafficBucketSize, upperBound)
269+
}
270+
271+
if upperBound < uint64(maxBucket) {
272+
// idx is offset, since there is no 0-16 bucket reported by BIND.
273+
idx := (upperBound+1)/bind.TrafficBucketSize - 2
274+
trafficHist[idx] += v
275+
} else {
276+
// Final slice element aggregates packet sizes from maxBucket to +Inf.
277+
trafficHist[len(trafficHist)-1] += v
278+
}
279+
}
280+
281+
return trafficHist, nil
282+
}

Diff for: bind_exporter.go

+101-5
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ var (
218218
"Zone serial number.",
219219
[]string{"view", "zone_name"}, nil,
220220
)
221+
trafficReceived = prometheus.NewDesc(
222+
prometheus.BuildFQName(namespace, "traffic", "received_size"),
223+
"Received traffic packet sizes.",
224+
[]string{"transport"}, nil,
225+
)
226+
trafficSent = prometheus.NewDesc(
227+
prometheus.BuildFQName(namespace, "traffic", "sent_size"),
228+
"Received traffic packet sizes.",
229+
[]string{"transport"}, nil,
230+
)
221231
)
222232

223233
type collectorConstructor func(log.Logger, *bind.Statistics) prometheus.Collector
@@ -387,6 +397,87 @@ func (c *taskCollector) Collect(ch chan<- prometheus.Metric) {
387397
)
388398
}
389399

400+
type trafficCollector struct {
401+
logger log.Logger
402+
stats *bind.Statistics
403+
}
404+
405+
// newTrafficCollector implements collectorConstructor.
406+
func newTrafficCollector(logger log.Logger, s *bind.Statistics) prometheus.Collector {
407+
return &trafficCollector{logger: logger, stats: s}
408+
}
409+
410+
// Describe implements prometheus.Collector.
411+
func (c *trafficCollector) Describe(ch chan<- *prometheus.Desc) {
412+
ch <- trafficReceived
413+
ch <- trafficSent
414+
}
415+
416+
// Collect implements prometheus.Collector.
417+
func (c *trafficCollector) Collect(ch chan<- prometheus.Metric) {
418+
// IPv4 traffic histograms.
419+
buckets, count := c.makeHistogram(c.stats.TrafficHistograms.ReceivedUDPv4)
420+
ch <- prometheus.MustNewConstHistogram(
421+
trafficReceived, count, math.NaN(), buckets, "udpv4",
422+
)
423+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.SentUDPv4)
424+
ch <- prometheus.MustNewConstHistogram(
425+
trafficSent, count, math.NaN(), buckets, "udpv4",
426+
)
427+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.ReceivedTCPv4)
428+
ch <- prometheus.MustNewConstHistogram(
429+
trafficReceived, count, math.NaN(), buckets, "tcpv4",
430+
)
431+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.SentTCPv4)
432+
ch <- prometheus.MustNewConstHistogram(
433+
trafficSent, count, math.NaN(), buckets, "tcpv4",
434+
)
435+
436+
// IPv6 traffic histograms.
437+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.ReceivedUDPv6)
438+
ch <- prometheus.MustNewConstHistogram(
439+
trafficReceived, count, math.NaN(), buckets, "udpv6",
440+
)
441+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.SentUDPv6)
442+
ch <- prometheus.MustNewConstHistogram(
443+
trafficSent, count, math.NaN(), buckets, "udpv6",
444+
)
445+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.ReceivedTCPv6)
446+
ch <- prometheus.MustNewConstHistogram(
447+
trafficReceived, count, math.NaN(), buckets, "tcpv6",
448+
)
449+
buckets, count = c.makeHistogram(c.stats.TrafficHistograms.SentTCPv6)
450+
ch <- prometheus.MustNewConstHistogram(
451+
trafficSent, count, math.NaN(), buckets, "tcpv6",
452+
)
453+
}
454+
455+
// makeHistogram translates the non-aggregated bucket slice into an aggregated map, suitable for
456+
// use by prometheus.MustNewConstHistogram().
457+
func (c *trafficCollector) makeHistogram(rawBuckets []uint64) (map[float64]uint64, uint64) {
458+
var (
459+
buckets = map[float64]uint64{}
460+
count uint64
461+
)
462+
463+
for i, v := range rawBuckets {
464+
if v > 0 {
465+
var idx float64
466+
467+
if i == len(rawBuckets)-1 {
468+
idx = math.Inf(1)
469+
} else {
470+
idx = float64((i+2)*bind.TrafficBucketSize) - 1
471+
}
472+
473+
count += v
474+
buckets[idx] = count
475+
}
476+
}
477+
478+
return buckets, count
479+
}
480+
390481
// Exporter collects Binds stats from the given server and exports them using
391482
// the prometheus metrics package.
392483
type Exporter struct {
@@ -411,10 +502,12 @@ func NewExporter(logger log.Logger, version, url string, timeout time.Duration,
411502
switch g {
412503
case bind.ServerStats:
413504
cs = append(cs, newServerCollector)
414-
case bind.ViewStats:
415-
cs = append(cs, newViewCollector)
416505
case bind.TaskStats:
417506
cs = append(cs, newTaskCollector)
507+
case bind.TrafficStats:
508+
cs = append(cs, newTrafficCollector)
509+
case bind.ViewStats:
510+
cs = append(cs, newViewCollector)
418511
}
419512
}
420513

@@ -503,10 +596,12 @@ func (s *statisticGroups) Set(value string) error {
503596
switch dt {
504597
case string(bind.ServerStats):
505598
sg = bind.ServerStats
506-
case string(bind.ViewStats):
507-
sg = bind.ViewStats
508599
case string(bind.TaskStats):
509600
sg = bind.TaskStats
601+
case string(bind.TrafficStats):
602+
sg = bind.TrafficStats
603+
case string(bind.ViewStats):
604+
sg = bind.ViewStats
510605
default:
511606
return fmt.Errorf("unknown stats group %q", dt)
512607
}
@@ -544,7 +639,8 @@ func main() {
544639
toolkitFlags := webflag.AddFlags(kingpin.CommandLine, ":9119")
545640

546641
kingpin.Flag("bind.stats-groups",
547-
"Comma-separated list of statistics to collect",
642+
"Comma-separated list of statistics to collect. "+
643+
"One or more of: [server, tasks, traffic, view]",
548644
).Default((&statisticGroups{
549645
bind.ServerStats, bind.ViewStats,
550646
}).String()).SetValue(&groups)

0 commit comments

Comments
 (0)