github.com/netdata/go.d.plugin@v0.58.1/modules/redis/collect_info.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package redis
     4  
     5  import (
     6  	"bufio"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/netdata/go.d.plugin/agent/module"
    13  )
    14  
    15  const (
    16  	infoSectionServer       = "# Server"
    17  	infoSectionData         = "# Data"
    18  	infoSectionClients      = "# Clients"
    19  	infoSectionStats        = "# Stats"
    20  	infoSectionCommandstats = "# Commandstats"
    21  	infoSectionCPU          = "# CPU"
    22  	infoSectionRepl         = "# Replication"
    23  	infoSectionKeyspace     = "# Keyspace"
    24  )
    25  
    26  var infoSections = map[string]struct{}{
    27  	infoSectionServer:       {},
    28  	infoSectionData:         {},
    29  	infoSectionClients:      {},
    30  	infoSectionStats:        {},
    31  	infoSectionCommandstats: {},
    32  	infoSectionCPU:          {},
    33  	infoSectionRepl:         {},
    34  	infoSectionKeyspace:     {},
    35  }
    36  
    37  func isInfoSection(line string) bool { _, ok := infoSections[line]; return ok }
    38  
    39  func (r *Redis) collectInfo(mx map[string]int64, info string) {
    40  	// https://redis.io/commands/info
    41  	// Lines can contain a section name (starting with a # character) or a property.
    42  	// All the properties are in the form of field:value terminated by \r\n.
    43  
    44  	var curSection string
    45  	sc := bufio.NewScanner(strings.NewReader(info))
    46  	for sc.Scan() {
    47  		line := strings.TrimSpace(sc.Text())
    48  		if len(line) == 0 {
    49  			curSection = ""
    50  			continue
    51  		}
    52  		if strings.HasPrefix(line, "#") {
    53  			if isInfoSection(line) {
    54  				curSection = line
    55  			}
    56  			continue
    57  		}
    58  
    59  		field, value, ok := parseProperty(line)
    60  		if !ok {
    61  			continue
    62  		}
    63  
    64  		switch {
    65  		case curSection == infoSectionCommandstats:
    66  			r.collectInfoCommandstatsProperty(mx, field, value)
    67  		case curSection == infoSectionKeyspace:
    68  			r.collectInfoKeyspaceProperty(mx, field, value)
    69  		case field == "rdb_last_bgsave_status":
    70  			collectNumericValue(mx, field, convertBgSaveStatus(value))
    71  		case field == "rdb_current_bgsave_time_sec" && value == "-1":
    72  			// TODO: https://github.com/netdata/dashboard/issues/198
    73  			// "-1" means there is no on-going bgsave operation;
    74  			// netdata has 'Convert seconds to time' feature (enabled by default),
    75  			// looks like it doesn't respect negative values and does abs().
    76  			// "-1" => "00:00:01".
    77  			collectNumericValue(mx, field, "0")
    78  		case field == "rdb_last_save_time":
    79  			v, _ := strconv.ParseInt(value, 10, 64)
    80  			mx[field] = int64(time.Since(time.Unix(v, 0)).Seconds())
    81  		case field == "aof_enabled" && value == "1":
    82  			r.addAOFChartsOnce.Do(r.addAOFCharts)
    83  		case field == "master_link_status":
    84  			mx["master_link_status_up"] = boolToInt(value == "up")
    85  			mx["master_link_status_down"] = boolToInt(value == "down")
    86  		default:
    87  			collectNumericValue(mx, field, value)
    88  		}
    89  	}
    90  
    91  	if has(mx, "keyspace_hits", "keyspace_misses") {
    92  		mx["keyspace_hit_rate"] = int64(calcKeyspaceHitRate(mx) * precision)
    93  	}
    94  	if has(mx, "master_last_io_seconds_ago") {
    95  		r.addReplSlaveChartsOnce.Do(r.addReplSlaveCharts)
    96  		if !has(mx, "master_link_down_since_seconds") {
    97  			mx["master_link_down_since_seconds"] = 0
    98  		}
    99  	}
   100  }
   101  
   102  var reKeyspaceValue = regexp.MustCompile(`^keys=(\d+),expires=(\d+)`)
   103  
   104  func (r *Redis) collectInfoKeyspaceProperty(ms map[string]int64, field, value string) {
   105  	match := reKeyspaceValue.FindStringSubmatch(value)
   106  	if match == nil {
   107  		return
   108  	}
   109  
   110  	keys, expires := match[1], match[2]
   111  	collectNumericValue(ms, field+"_keys", keys)
   112  	collectNumericValue(ms, field+"_expires_keys", expires)
   113  
   114  	if !r.collectedDbs[field] {
   115  		r.collectedDbs[field] = true
   116  		r.addDbToKeyspaceCharts(field)
   117  	}
   118  }
   119  
   120  var reCommandstatsValue = regexp.MustCompile(`^calls=(\d+),usec=(\d+),usec_per_call=([\d.]+)`)
   121  
   122  func (r *Redis) collectInfoCommandstatsProperty(ms map[string]int64, field, value string) {
   123  	if !strings.HasPrefix(field, "cmdstat_") {
   124  		return
   125  	}
   126  	cmd := field[len("cmdstat_"):]
   127  
   128  	match := reCommandstatsValue.FindStringSubmatch(value)
   129  	if match == nil {
   130  		return
   131  	}
   132  
   133  	calls, usec, usecPerCall := match[1], match[2], match[3]
   134  	collectNumericValue(ms, "cmd_"+cmd+"_calls", calls)
   135  	collectNumericValue(ms, "cmd_"+cmd+"_usec", usec)
   136  	collectNumericValue(ms, "cmd_"+cmd+"_usec_per_call", usecPerCall)
   137  
   138  	if !r.collectedCommands[cmd] {
   139  		r.collectedCommands[cmd] = true
   140  		r.addCmdToCommandsCharts(cmd)
   141  	}
   142  }
   143  
   144  func collectNumericValue(ms map[string]int64, field, value string) {
   145  	v, err := strconv.ParseFloat(value, 64)
   146  	if err != nil {
   147  		return
   148  	}
   149  	if strings.IndexByte(value, '.') == -1 {
   150  		ms[field] = int64(v)
   151  	} else {
   152  		ms[field] = int64(v * precision)
   153  	}
   154  }
   155  
   156  func convertBgSaveStatus(status string) string {
   157  	// https://github.com/redis/redis/blob/unstable/src/server.c
   158  	// "ok" or "err"
   159  	if status == "ok" {
   160  		return "0"
   161  	}
   162  	return "1"
   163  }
   164  
   165  func parseProperty(prop string) (field, value string, ok bool) {
   166  	i := strings.IndexByte(prop, ':')
   167  	if i == -1 {
   168  		return "", "", false
   169  	}
   170  	field, value = prop[:i], prop[i+1:]
   171  	return field, value, field != "" && value != ""
   172  }
   173  
   174  func calcKeyspaceHitRate(ms map[string]int64) float64 {
   175  	hits := ms["keyspace_hits"]
   176  	misses := ms["keyspace_misses"]
   177  	if hits+misses == 0 {
   178  		return 0
   179  	}
   180  	return float64(hits) * 100 / float64(hits+misses)
   181  }
   182  
   183  func (r *Redis) addCmdToCommandsCharts(cmd string) {
   184  	r.addDimToChart(chartCommandsCalls.ID, &module.Dim{
   185  		ID:   "cmd_" + cmd + "_calls",
   186  		Name: strings.ToUpper(cmd),
   187  		Algo: module.Incremental,
   188  	})
   189  	r.addDimToChart(chartCommandsUsec.ID, &module.Dim{
   190  		ID:   "cmd_" + cmd + "_usec",
   191  		Name: strings.ToUpper(cmd),
   192  		Algo: module.Incremental,
   193  	})
   194  	r.addDimToChart(chartCommandsUsecPerSec.ID, &module.Dim{
   195  		ID:   "cmd_" + cmd + "_usec_per_call",
   196  		Name: strings.ToUpper(cmd),
   197  		Div:  precision,
   198  	})
   199  }
   200  
   201  func (r *Redis) addDbToKeyspaceCharts(db string) {
   202  	r.addDimToChart(chartKeys.ID, &module.Dim{
   203  		ID:   db + "_keys",
   204  		Name: db,
   205  	})
   206  	r.addDimToChart(chartExpiresKeys.ID, &module.Dim{
   207  		ID:   db + "_expires_keys",
   208  		Name: db,
   209  	})
   210  }
   211  
   212  func (r *Redis) addDimToChart(chartID string, dim *module.Dim) {
   213  	chart := r.Charts().Get(chartID)
   214  	if chart == nil {
   215  		r.Warningf("error on adding '%s' dimension: can not find '%s' chart", dim.ID, chartID)
   216  		return
   217  	}
   218  	if err := chart.AddDim(dim); err != nil {
   219  		r.Warning(err)
   220  		return
   221  	}
   222  	chart.MarkNotCreated()
   223  }
   224  
   225  func (r *Redis) addAOFCharts() {
   226  	err := r.Charts().Add(chartPersistenceAOFSize.Copy())
   227  	if err != nil {
   228  		r.Warningf("error on adding '%s' chart", chartPersistenceAOFSize.ID)
   229  	}
   230  }
   231  
   232  func (r *Redis) addReplSlaveCharts() {
   233  	if err := r.Charts().Add(masterLinkStatusChart.Copy()); err != nil {
   234  		r.Warningf("error on adding '%s' chart", masterLinkStatusChart.ID)
   235  	}
   236  	if err := r.Charts().Add(masterLastIOSinceTimeChart.Copy()); err != nil {
   237  		r.Warningf("error on adding '%s' chart", masterLastIOSinceTimeChart.ID)
   238  	}
   239  	if err := r.Charts().Add(masterLinkDownSinceTimeChart.Copy()); err != nil {
   240  		r.Warningf("error on adding '%s' chart", masterLinkDownSinceTimeChart.ID)
   241  	}
   242  }
   243  
   244  func has(m map[string]int64, key string, keys ...string) bool {
   245  	switch _, ok := m[key]; len(keys) {
   246  	case 0:
   247  		return ok
   248  	default:
   249  		return ok && has(m, keys[0], keys[1:]...)
   250  	}
   251  }
   252  
   253  func boolToInt(v bool) int64 {
   254  	if v {
   255  		return 1
   256  	}
   257  	return 0
   258  }