firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/redis/collect_info.go

Summary

Maintainability
A
1 hr
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package redis

import (
    "bufio"
    "regexp"
    "strconv"
    "strings"
    "time"

    "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
)

const (
    infoSectionServer       = "# Server"
    infoSectionData         = "# Data"
    infoSectionClients      = "# Clients"
    infoSectionStats        = "# Stats"
    infoSectionCommandstats = "# Commandstats"
    infoSectionCPU          = "# CPU"
    infoSectionRepl         = "# Replication"
    infoSectionKeyspace     = "# Keyspace"
)

var infoSections = map[string]struct{}{
    infoSectionServer:       {},
    infoSectionData:         {},
    infoSectionClients:      {},
    infoSectionStats:        {},
    infoSectionCommandstats: {},
    infoSectionCPU:          {},
    infoSectionRepl:         {},
    infoSectionKeyspace:     {},
}

func isInfoSection(line string) bool { _, ok := infoSections[line]; return ok }

func (r *Redis) collectInfo(mx map[string]int64, info string) {
    // https://redis.io/commands/info
    // Lines can contain a section name (starting with a # character) or a property.
    // All the properties are in the form of field:value terminated by \r\n.

    var curSection string
    sc := bufio.NewScanner(strings.NewReader(info))
    for sc.Scan() {
        line := strings.TrimSpace(sc.Text())
        if len(line) == 0 {
            curSection = ""
            continue
        }
        if strings.HasPrefix(line, "#") {
            if isInfoSection(line) {
                curSection = line
            }
            continue
        }

        field, value, ok := parseProperty(line)
        if !ok {
            continue
        }

        switch {
        case curSection == infoSectionCommandstats:
            r.collectInfoCommandstatsProperty(mx, field, value)
        case curSection == infoSectionKeyspace:
            r.collectInfoKeyspaceProperty(mx, field, value)
        case field == "rdb_last_bgsave_status":
            collectNumericValue(mx, field, convertBgSaveStatus(value))
        case field == "rdb_current_bgsave_time_sec" && value == "-1":
            // TODO: https://github.com/netdata/dashboard/issues/198
            // "-1" means there is no on-going bgsave operation;
            // netdata has 'Convert seconds to time' feature (enabled by default),
            // looks like it doesn't respect negative values and does abs().
            // "-1" => "00:00:01".
            collectNumericValue(mx, field, "0")
        case field == "rdb_last_save_time":
            v, _ := strconv.ParseInt(value, 10, 64)
            mx[field] = int64(time.Since(time.Unix(v, 0)).Seconds())
        case field == "aof_enabled" && value == "1":
            r.addAOFChartsOnce.Do(r.addAOFCharts)
        case field == "master_link_status":
            mx["master_link_status_up"] = boolToInt(value == "up")
            mx["master_link_status_down"] = boolToInt(value == "down")
        default:
            collectNumericValue(mx, field, value)
        }
    }

    if has(mx, "keyspace_hits", "keyspace_misses") {
        mx["keyspace_hit_rate"] = int64(calcKeyspaceHitRate(mx) * precision)
    }
    if has(mx, "master_last_io_seconds_ago") {
        r.addReplSlaveChartsOnce.Do(r.addReplSlaveCharts)
        if !has(mx, "master_link_down_since_seconds") {
            mx["master_link_down_since_seconds"] = 0
        }
    }
}

var reKeyspaceValue = regexp.MustCompile(`^keys=(\d+),expires=(\d+)`)

func (r *Redis) collectInfoKeyspaceProperty(ms map[string]int64, field, value string) {
    match := reKeyspaceValue.FindStringSubmatch(value)
    if match == nil {
        return
    }

    keys, expires := match[1], match[2]
    collectNumericValue(ms, field+"_keys", keys)
    collectNumericValue(ms, field+"_expires_keys", expires)

    if !r.collectedDbs[field] {
        r.collectedDbs[field] = true
        r.addDbToKeyspaceCharts(field)
    }
}

var reCommandstatsValue = regexp.MustCompile(`^calls=(\d+),usec=(\d+),usec_per_call=([\d.]+)`)

func (r *Redis) collectInfoCommandstatsProperty(ms map[string]int64, field, value string) {
    if !strings.HasPrefix(field, "cmdstat_") {
        return
    }
    cmd := field[len("cmdstat_"):]

    match := reCommandstatsValue.FindStringSubmatch(value)
    if match == nil {
        return
    }

    calls, usec, usecPerCall := match[1], match[2], match[3]
    collectNumericValue(ms, "cmd_"+cmd+"_calls", calls)
    collectNumericValue(ms, "cmd_"+cmd+"_usec", usec)
    collectNumericValue(ms, "cmd_"+cmd+"_usec_per_call", usecPerCall)

    if !r.collectedCommands[cmd] {
        r.collectedCommands[cmd] = true
        r.addCmdToCommandsCharts(cmd)
    }
}

func collectNumericValue(ms map[string]int64, field, value string) {
    v, err := strconv.ParseFloat(value, 64)
    if err != nil {
        return
    }
    if strings.IndexByte(value, '.') == -1 {
        ms[field] = int64(v)
    } else {
        ms[field] = int64(v * precision)
    }
}

func convertBgSaveStatus(status string) string {
    // https://github.com/redis/redis/blob/unstable/src/server.c
    // "ok" or "err"
    if status == "ok" {
        return "0"
    }
    return "1"
}

func parseProperty(prop string) (field, value string, ok bool) {
    i := strings.IndexByte(prop, ':')
    if i == -1 {
        return "", "", false
    }
    field, value = prop[:i], prop[i+1:]
    return field, value, field != "" && value != ""
}

func calcKeyspaceHitRate(ms map[string]int64) float64 {
    hits := ms["keyspace_hits"]
    misses := ms["keyspace_misses"]
    if hits+misses == 0 {
        return 0
    }
    return float64(hits) * 100 / float64(hits+misses)
}

func (r *Redis) addCmdToCommandsCharts(cmd string) {
    r.addDimToChart(chartCommandsCalls.ID, &module.Dim{
        ID:   "cmd_" + cmd + "_calls",
        Name: strings.ToUpper(cmd),
        Algo: module.Incremental,
    })
    r.addDimToChart(chartCommandsUsec.ID, &module.Dim{
        ID:   "cmd_" + cmd + "_usec",
        Name: strings.ToUpper(cmd),
        Algo: module.Incremental,
    })
    r.addDimToChart(chartCommandsUsecPerSec.ID, &module.Dim{
        ID:   "cmd_" + cmd + "_usec_per_call",
        Name: strings.ToUpper(cmd),
        Div:  precision,
    })
}

func (r *Redis) addDbToKeyspaceCharts(db string) {
    r.addDimToChart(chartKeys.ID, &module.Dim{
        ID:   db + "_keys",
        Name: db,
    })
    r.addDimToChart(chartExpiresKeys.ID, &module.Dim{
        ID:   db + "_expires_keys",
        Name: db,
    })
}

func (r *Redis) addDimToChart(chartID string, dim *module.Dim) {
    chart := r.Charts().Get(chartID)
    if chart == nil {
        r.Warningf("error on adding '%s' dimension: can not find '%s' chart", dim.ID, chartID)
        return
    }
    if err := chart.AddDim(dim); err != nil {
        r.Warning(err)
        return
    }
    chart.MarkNotCreated()
}

func (r *Redis) addAOFCharts() {
    err := r.Charts().Add(chartPersistenceAOFSize.Copy())
    if err != nil {
        r.Warningf("error on adding '%s' chart", chartPersistenceAOFSize.ID)
    }
}

func (r *Redis) addReplSlaveCharts() {
    if err := r.Charts().Add(masterLinkStatusChart.Copy()); err != nil {
        r.Warningf("error on adding '%s' chart", masterLinkStatusChart.ID)
    }
    if err := r.Charts().Add(masterLastIOSinceTimeChart.Copy()); err != nil {
        r.Warningf("error on adding '%s' chart", masterLastIOSinceTimeChart.ID)
    }
    if err := r.Charts().Add(masterLinkDownSinceTimeChart.Copy()); err != nil {
        r.Warningf("error on adding '%s' chart", masterLinkDownSinceTimeChart.ID)
    }
}

func has(m map[string]int64, key string, keys ...string) bool {
    switch _, ok := m[key]; len(keys) {
    case 0:
        return ok
    default:
        return ok && has(m, keys[0], keys[1:]...)
    }
}

func boolToInt(v bool) int64 {
    if v {
        return 1
    }
    return 0
}