summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/redis/collect_info.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/go/collectors/go.d.plugin/modules/redis/collect_info.go258
1 files changed, 258 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/redis/collect_info.go b/src/go/collectors/go.d.plugin/modules/redis/collect_info.go
new file mode 100644
index 000000000..ce6cae062
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/redis/collect_info.go
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package redis
+
+import (
+ "bufio"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/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
+}