master
go 209 lines 5.92 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package unbound
4
5 import (
6 "fmt"
7 "strconv"
8 "strings"
9 )
10
11 // https://github.com/NLnetLabs/unbound/blob/master/daemon/remote.c (do_stats: print_stats, print_thread_stats, print_mem, print_uptime, print_ext)
12 // https://github.com/NLnetLabs/unbound/blob/master/libunbound/unbound.h (structs: ub_server_stats, ub_shm_stat_info)
13 // https://docs.datadoghq.com/integrations/unbound/#metrics (stats description)
14 // https://docs.menandmice.com/display/MM/Unbound+request-list+demystified (request lists explanation)
15
16 func (c *Collector) collect() (map[string]int64, error) {
17 stats, err := c.scrapeUnboundStats()
18 if err != nil {
19 return nil, err
20 }
21
22 mx := c.collectStats(stats)
23 c.updateCharts()
24 return mx, nil
25 }
26
27 func (c *Collector) scrapeUnboundStats() ([]entry, error) {
28 var output []string
29 var command = "UBCT1 stats"
30 if c.Cumulative {
31 command = "UBCT1 stats_noreset"
32 }
33
34 if err := c.client.Connect(); err != nil {
35 return nil, fmt.Errorf("failed to connect: %v", err)
36 }
37 defer func() { _ = c.client.Disconnect() }()
38
39 err := c.client.Command(command+"\n", func(bytes []byte) (bool, error) {
40 output = append(output, string(bytes))
41 return true, nil
42 })
43 if err != nil {
44 return nil, fmt.Errorf("send command '%s': %w", command, err)
45 }
46
47 switch len(output) {
48 case 0:
49 return nil, fmt.Errorf("command '%s': empty resopnse", command)
50 case 1:
51 // in case of error the first line of the response is: error <descriptive text possible> \n
52 return nil, fmt.Errorf("command '%s': '%s'", command, output[0])
53 }
54 return parseStatsOutput(output)
55 }
56
57 func (c *Collector) collectStats(stats []entry) map[string]int64 {
58 if c.Cumulative {
59 return c.collectCumulativeStats(stats)
60 }
61 return c.collectNonCumulativeStats(stats)
62 }
63
64 func (c *Collector) collectCumulativeStats(stats []entry) map[string]int64 {
65 mul := float64(1000)
66 // following stats change only on cachemiss event in cumulative mode
67 // - *.requestlist.avg,
68 // - *.recursion.time.avg
69 // - *.recursion.time.median
70 v := findEntry("total.num.cachemiss", stats)
71 if v == c.prevCacheMiss {
72 // so we need to reset them if there is no such event
73 mul = 0
74 }
75 c.prevCacheMiss = v
76 return c.processStats(stats, mul)
77 }
78
79 func (c *Collector) collectNonCumulativeStats(stats []entry) map[string]int64 {
80 mul := float64(1000)
81 mx := c.processStats(stats, mul)
82
83 // see 'static int print_ext(RES* ssl, struct ub_stats_info* s)' in
84 // https://github.com/NLnetLabs/unbound/blob/master/daemon/remote.c
85 // - zero value queries type not included
86 // - zero value queries class not included
87 // - zero value queries opcode not included
88 // - only 0-6 rcodes answers always included, other zero value rcodes not included
89 for k := range c.cache.queryType {
90 if _, ok := c.curCache.queryType[k]; !ok {
91 mx["num.query.type."+k] = 0
92 }
93 }
94 for k := range c.cache.queryClass {
95 if _, ok := c.curCache.queryClass[k]; !ok {
96 mx["num.query.class."+k] = 0
97 }
98 }
99 for k := range c.cache.queryOpCode {
100 if _, ok := c.curCache.queryOpCode[k]; !ok {
101 mx["num.query.opcode."+k] = 0
102 }
103 }
104 for k := range c.cache.answerRCode {
105 if _, ok := c.curCache.answerRCode[k]; !ok {
106 mx["num.answer.rcode."+k] = 0
107 }
108 }
109 return mx
110 }
111
112 func (c *Collector) processStats(stats []entry, mul float64) map[string]int64 {
113 c.curCache.clear()
114 mx := make(map[string]int64, len(stats))
115 for _, e := range stats {
116 switch {
117 // *.requestlist.avg, *.recursion.time.avg, *.recursion.time.median
118 case e.hasSuffix(".avg"), e.hasSuffix(".median"):
119 e.value *= mul
120 case e.hasPrefix("thread") && e.hasSuffix("num.queries"):
121 v := extractThread(e.key)
122 c.curCache.threads[v] = true
123 case e.hasPrefix("num.query.type"):
124 v := extractQueryType(e.key)
125 c.curCache.queryType[v] = true
126 case e.hasPrefix("num.query.class"):
127 v := extractQueryClass(e.key)
128 c.curCache.queryClass[v] = true
129 case e.hasPrefix("num.query.opcode"):
130 v := extractQueryOpCode(e.key)
131 c.curCache.queryOpCode[v] = true
132 case e.hasPrefix("num.answer.rcode"):
133 v := extractAnswerRCode(e.key)
134 c.curCache.answerRCode[v] = true
135 }
136 mx[e.key] = int64(e.value)
137 }
138 return mx
139 }
140
141 func extractThread(key string) string { before, _, _ := strings.Cut(key, "."); return before }
142 func extractQueryType(key string) string { i := len("num.query.type."); return key[i:] }
143 func extractQueryClass(key string) string { i := len("num.query.class."); return key[i:] }
144 func extractQueryOpCode(key string) string { i := len("num.query.opcode."); return key[i:] }
145 func extractAnswerRCode(key string) string { i := len("num.answer.rcode."); return key[i:] }
146
147 type entry struct {
148 key string
149 value float64
150 }
151
152 func (e entry) hasPrefix(prefix string) bool { return strings.HasPrefix(e.key, prefix) }
153 func (e entry) hasSuffix(suffix string) bool { return strings.HasSuffix(e.key, suffix) }
154
155 func findEntry(key string, entries []entry) float64 {
156 for _, e := range entries {
157 if e.key == key {
158 return e.value
159 }
160 }
161 return -1
162 }
163
164 func parseStatsOutput(output []string) ([]entry, error) {
165 var es []entry
166 for _, v := range output {
167 e, err := parseStatsLine(v)
168 if err != nil {
169 return nil, err
170 }
171 if e.hasPrefix("histogram") {
172 continue
173 }
174 es = append(es, e)
175 }
176 return es, nil
177 }
178
179 func parseStatsLine(line string) (entry, error) {
180 // 'stats' output is a list of [key]=[value] lines.
181 parts := strings.Split(line, "=")
182 if len(parts) != 2 {
183 return entry{}, fmt.Errorf("bad line syntax: %s", line)
184 }
185 f, err := strconv.ParseFloat(parts[1], 64)
186 return entry{key: parts[0], value: f}, err
187 }
188
189 func newCollectCache() collectCache {
190 return collectCache{
191 threads: make(map[string]bool),
192 queryType: make(map[string]bool),
193 queryClass: make(map[string]bool),
194 queryOpCode: make(map[string]bool),
195 answerRCode: make(map[string]bool),
196 }
197 }
198
199 type collectCache struct {
200 threads map[string]bool
201 queryType map[string]bool
202 queryClass map[string]bool
203 queryOpCode map[string]bool
204 answerRCode map[string]bool
205 }
206
207 func (c *collectCache) clear() {
208 *c = newCollectCache()
209 }