netdata/netdata

View on GitHub
src/go/collectors/go.d.plugin/modules/unbound/collect.go

Summary

Maintainability
A
0 mins
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package unbound

import (
    "fmt"
    "strconv"
    "strings"
)

// https://github.com/NLnetLabs/unbound/blob/master/daemon/remote.c (do_stats: print_stats, print_thread_stats, print_mem, print_uptime, print_ext)
// https://github.com/NLnetLabs/unbound/blob/master/libunbound/unbound.h (structs: ub_server_stats, ub_shm_stat_info)
// https://docs.datadoghq.com/integrations/unbound/#metrics (stats description)
// https://docs.menandmice.com/display/MM/Unbound+request-list+demystified (request lists explanation)

func (u *Unbound) collect() (map[string]int64, error) {
    stats, err := u.scrapeUnboundStats()
    if err != nil {
        return nil, err
    }

    mx := u.collectStats(stats)
    u.updateCharts()
    return mx, nil
}

func (u *Unbound) scrapeUnboundStats() ([]entry, error) {
    var output []string
    var command = "UBCT1 stats"
    if u.Cumulative {
        command = "UBCT1 stats_noreset"
    }

    if err := u.client.Connect(); err != nil {
        return nil, fmt.Errorf("failed to connect: %v", err)
    }
    defer func() { _ = u.client.Disconnect() }()

    err := u.client.Command(command+"\n", func(bytes []byte) bool {
        output = append(output, string(bytes))
        return true
    })
    if err != nil {
        return nil, fmt.Errorf("send command '%s': %w", command, err)
    }

    switch len(output) {
    case 0:
        return nil, fmt.Errorf("command '%s': empty resopnse", command)
    case 1:
        //     in case of error the first line of the response is: error <descriptive text possible> \n
        return nil, fmt.Errorf("command '%s': '%s'", command, output[0])
    }
    return parseStatsOutput(output)
}

func (u *Unbound) collectStats(stats []entry) map[string]int64 {
    if u.Cumulative {
        return u.collectCumulativeStats(stats)
    }
    return u.collectNonCumulativeStats(stats)
}

func (u *Unbound) collectCumulativeStats(stats []entry) map[string]int64 {
    mul := float64(1000)
    // following stats change only on cachemiss event in cumulative mode
    // - *.requestlist.avg,
    // - *.recursion.time.avg
    // - *.recursion.time.median
    v := findEntry("total.num.cachemiss", stats)
    if v == u.prevCacheMiss {
        // so we need to reset them if there is no such event
        mul = 0
    }
    u.prevCacheMiss = v
    return u.processStats(stats, mul)
}

func (u *Unbound) collectNonCumulativeStats(stats []entry) map[string]int64 {
    mul := float64(1000)
    mx := u.processStats(stats, mul)

    // see 'static int print_ext(RES* ssl, struct ub_stats_info* s)' in
    // https://github.com/NLnetLabs/unbound/blob/master/daemon/remote.c
    // - zero value queries type not included
    // - zero value queries class not included
    // - zero value queries opcode not included
    // - only 0-6 rcodes answers always included, other zero value rcodes not included
    for k := range u.cache.queryType {
        if _, ok := u.curCache.queryType[k]; !ok {
            mx["num.query.type."+k] = 0
        }
    }
    for k := range u.cache.queryClass {
        if _, ok := u.curCache.queryClass[k]; !ok {
            mx["num.query.class."+k] = 0
        }
    }
    for k := range u.cache.queryOpCode {
        if _, ok := u.curCache.queryOpCode[k]; !ok {
            mx["num.query.opcode."+k] = 0
        }
    }
    for k := range u.cache.answerRCode {
        if _, ok := u.curCache.answerRCode[k]; !ok {
            mx["num.answer.rcode."+k] = 0
        }
    }
    return mx
}

func (u *Unbound) processStats(stats []entry, mul float64) map[string]int64 {
    u.curCache.clear()
    mx := make(map[string]int64, len(stats))
    for _, e := range stats {
        switch {
        //     *.requestlist.avg, *.recursion.time.avg, *.recursion.time.median
        case e.hasSuffix(".avg"), e.hasSuffix(".median"):
            e.value *= mul
        case e.hasPrefix("thread") && e.hasSuffix("num.queries"):
            v := extractThread(e.key)
            u.curCache.threads[v] = true
        case e.hasPrefix("num.query.type"):
            v := extractQueryType(e.key)
            u.curCache.queryType[v] = true
        case e.hasPrefix("num.query.class"):
            v := extractQueryClass(e.key)
            u.curCache.queryClass[v] = true
        case e.hasPrefix("num.query.opcode"):
            v := extractQueryOpCode(e.key)
            u.curCache.queryOpCode[v] = true
        case e.hasPrefix("num.answer.rcode"):
            v := extractAnswerRCode(e.key)
            u.curCache.answerRCode[v] = true
        }
        mx[e.key] = int64(e.value)
    }
    return mx
}

func extractThread(key string) string      { idx := strings.IndexByte(key, '.'); return key[:idx] }
func extractQueryType(key string) string   { i := len("num.query.type."); return key[i:] }
func extractQueryClass(key string) string  { i := len("num.query.class."); return key[i:] }
func extractQueryOpCode(key string) string { i := len("num.query.opcode."); return key[i:] }
func extractAnswerRCode(key string) string { i := len("num.answer.rcode."); return key[i:] }

type entry struct {
    key   string
    value float64
}

func (e entry) hasPrefix(prefix string) bool { return strings.HasPrefix(e.key, prefix) }
func (e entry) hasSuffix(suffix string) bool { return strings.HasSuffix(e.key, suffix) }

func findEntry(key string, entries []entry) float64 {
    for _, e := range entries {
        if e.key == key {
            return e.value
        }
    }
    return -1
}

func parseStatsOutput(output []string) ([]entry, error) {
    var es []entry
    for _, v := range output {
        e, err := parseStatsLine(v)
        if err != nil {
            return nil, err
        }
        if e.hasPrefix("histogram") {
            continue
        }
        es = append(es, e)
    }
    return es, nil
}

func parseStatsLine(line string) (entry, error) {
    // 'stats' output is a list of [key]=[value] lines.
    parts := strings.Split(line, "=")
    if len(parts) != 2 {
        return entry{}, fmt.Errorf("bad line syntax: %s", line)
    }
    f, err := strconv.ParseFloat(parts[1], 64)
    return entry{key: parts[0], value: f}, err
}

func newCollectCache() collectCache {
    return collectCache{
        threads:     make(map[string]bool),
        queryType:   make(map[string]bool),
        queryClass:  make(map[string]bool),
        queryOpCode: make(map[string]bool),
        answerRCode: make(map[string]bool),
    }
}

type collectCache struct {
    threads     map[string]bool
    queryType   map[string]bool
    queryClass  map[string]bool
    queryOpCode map[string]bool
    answerRCode map[string]bool
}

func (c *collectCache) clear() {
    *c = newCollectCache()
}