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