github.com/netdata/go.d.plugin@v0.58.1/modules/unbound/collect.go (about) 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 (u *Unbound) collect() (map[string]int64, error) { 17 stats, err := u.scrapeUnboundStats() 18 if err != nil { 19 return nil, err 20 } 21 22 mx := u.collectStats(stats) 23 u.updateCharts() 24 return mx, nil 25 } 26 27 func (u *Unbound) scrapeUnboundStats() ([]entry, error) { 28 var output []string 29 var command = "UBCT1 stats" 30 if u.Cumulative { 31 command = "UBCT1 stats_noreset" 32 } 33 34 if err := u.client.Connect(); err != nil { 35 return nil, fmt.Errorf("failed to connect: %v", err) 36 } 37 defer func() { _ = u.client.Disconnect() }() 38 39 err := u.client.Command(command+"\n", func(bytes []byte) bool { 40 output = append(output, string(bytes)) 41 return true 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 (u *Unbound) collectStats(stats []entry) map[string]int64 { 58 if u.Cumulative { 59 return u.collectCumulativeStats(stats) 60 } 61 return u.collectNonCumulativeStats(stats) 62 } 63 64 func (u *Unbound) 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 == u.prevCacheMiss { 72 // so we need to reset them if there is no such event 73 mul = 0 74 } 75 u.prevCacheMiss = v 76 return u.processStats(stats, mul) 77 } 78 79 func (u *Unbound) collectNonCumulativeStats(stats []entry) map[string]int64 { 80 mul := float64(1000) 81 mx := u.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 u.cache.queryType { 90 if _, ok := u.curCache.queryType[k]; !ok { 91 mx["num.query.type."+k] = 0 92 } 93 } 94 for k := range u.cache.queryClass { 95 if _, ok := u.curCache.queryClass[k]; !ok { 96 mx["num.query.class."+k] = 0 97 } 98 } 99 for k := range u.cache.queryOpCode { 100 if _, ok := u.curCache.queryOpCode[k]; !ok { 101 mx["num.query.opcode."+k] = 0 102 } 103 } 104 for k := range u.cache.answerRCode { 105 if _, ok := u.curCache.answerRCode[k]; !ok { 106 mx["num.answer.rcode."+k] = 0 107 } 108 } 109 return mx 110 } 111 112 func (u *Unbound) processStats(stats []entry, mul float64) map[string]int64 { 113 u.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 u.curCache.threads[v] = true 123 case e.hasPrefix("num.query.type"): 124 v := extractQueryType(e.key) 125 u.curCache.queryType[v] = true 126 case e.hasPrefix("num.query.class"): 127 v := extractQueryClass(e.key) 128 u.curCache.queryClass[v] = true 129 case e.hasPrefix("num.query.opcode"): 130 v := extractQueryOpCode(e.key) 131 u.curCache.queryOpCode[v] = true 132 case e.hasPrefix("num.answer.rcode"): 133 v := extractAnswerRCode(e.key) 134 u.curCache.answerRCode[v] = true 135 } 136 mx[e.key] = int64(e.value) 137 } 138 return mx 139 } 140 141 func extractThread(key string) string { idx := strings.IndexByte(key, '.'); return key[:idx] } 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 }