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  }