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 }