Skip to content

Commit 3805c6f

Browse files
authored
Misc bug fixes (#1085)
* Fix panic in client-output-buffer-limit parsing when value length is not a multiple of 4 * Fix panic in extractTile38Metrics when info slice has odd length * Fix panic in extractModulesMetrics when module fields lack '=' delimiter * Fix panic in extractLatencyHistogramMetrics when reply or bucketInfo has odd length * Fix panic in extractSlowLogMetrics from bare type assertions on Redis values * Fix getStreamEntryId: bounds check before access, remove dead code and redundant type assertion * Remove expired_time_cap_reached_count from gauge map (it is a counter) * Fix extractTotalUsecForCommand matching unrelated commands via HasPrefix (e.g. get matching getex) * Fix non-pipelined path to emit key_size=0 for missing keys, matching pipelined behavior * Deduplicate cluster connection: create once and reuse for key checks and key groups * Pre-compile regexes in parseClientListString and parseSentinelMasterString
1 parent a26d7eb commit 3805c6f

File tree

9 files changed

+61
-57
lines changed

9 files changed

+61
-57
lines changed

exporter/clients.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
log "github.com/sirupsen/logrus"
1212
)
1313

14+
var reClientListId = regexp.MustCompile(`^id=\d+ addr=\S+`)
15+
1416
type ClientInfo struct {
1517
Id,
1618
Name,
@@ -41,7 +43,7 @@ id=14 addr=127.0.0.1:64958 fd=9 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 mul
4143
id=40253233 addr=fd40:1481:21:dbe0:7021:300:a03:1a06:44426 fd=19 name= age=782 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 ow=0 owmem=0 events=r cmd=client user=default lib-name=redis-py lib-ver=5.0.1 numops=9
4244
*/
4345
func parseClientListString(clientInfo string) (*ClientInfo, bool) {
44-
if matched, _ := regexp.MatchString(`^id=\d+ addr=\S+`, clientInfo); !matched {
46+
if !reClientListId.MatchString(clientInfo) {
4547
return nil, false
4648
}
4749
connectedClient := ClientInfo{}

exporter/exporter.go

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,6 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) {
222222
"active_defrag_key_hits": "defrag_key_hits",
223223
"active_defrag_key_misses": "defrag_key_misses",
224224

225-
// https://github.com/antirez/redis/blob/0af467d18f9d12b137af3b709c0af579c29d8414/src/expire.c#L297-L299
226-
"expired_time_cap_reached_count": "expired_time_cap_reached_total",
227-
228225
// # Persistence
229226
"loading": "loading_dump_file",
230227
"async_loading": "async_loading", // Added in Redis 7.0
@@ -730,7 +727,7 @@ func (e *Exporter) extractConfigMetrics(ch chan<- prometheus.Metric, config []in
730727
if strKey == "client-output-buffer-limit" {
731728
// client-output-buffer-limit "normal 0 0 0 slave 1610612736 1610612736 0 pubsub 33554432 8388608 60"
732729
splitVal := strings.Split(strVal, " ")
733-
for i := 0; i < len(splitVal); i += 4 {
730+
for i := 0; i+3 < len(splitVal); i += 4 {
734731
class := splitVal[i]
735732
if val, err := strconv.ParseFloat(splitVal[i+1], 64); err == nil {
736733
e.registerConstMetricGauge(ch, "config_client_output_buffer_limit_bytes", val, class, "hard")
@@ -747,15 +744,6 @@ func (e *Exporter) extractConfigMetrics(ch chan<- prometheus.Metric, config []in
747744
return
748745
}
749746

750-
// getKeyOperationConnection returns the appropriate Redis connection for key-based operations.
751-
// For cluster mode, it returns a cluster connection; otherwise, it returns the provided connection.
752-
func (e *Exporter) getKeyOperationConnection(defaultConn redis.Conn) (redis.Conn, error) {
753-
if e.options.IsCluster {
754-
return e.connectToRedisCluster()
755-
}
756-
return defaultConn, nil
757-
}
758-
759747
func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error {
760748
defer log.Debugf("scrapeRedisHost() done")
761749

@@ -849,27 +837,38 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error {
849837
e.extractLatencyMetrics(ch, infoAll, c)
850838
}
851839

840+
var keyConn redis.Conn
841+
if e.options.IsCluster {
842+
//
843+
// in cluster mode we need to create a new, cluster-aware connection
844+
// to properly handle cluster-redirects
845+
//
846+
k, keyConnErr := e.connectToRedisCluster()
847+
if keyConnErr != nil {
848+
log.Errorf("failed to get key operation connection: %s", keyConnErr)
849+
} else {
850+
//
851+
// The "defer keyConn.Close()" ensures this temporary cluster connection gets
852+
// cleaned up after key metrics extraction, while avoiding closing the original
853+
// connection "c" which is still needed.
854+
//
855+
defer k.Close()
856+
keyConn = k
857+
}
858+
} else {
859+
// not in cluster mode - we can use our existing connection for retrieving keys and such
860+
keyConn = c
861+
}
862+
852863
// skip these metrics for master if SkipCheckKeysForRoleMaster is set
853864
// (can help with reducing workload on the master node)
854865
log.Debugf("checkKeys metric collection for role: %s SkipCheckKeysForRoleMaster flag: %#v", role, e.options.SkipCheckKeysForRoleMaster)
855866
if role == InstanceRoleSlave || !e.options.SkipCheckKeysForRoleMaster {
856-
// For key-based operations, use cluster connection if in cluster mode
857-
keyConn, err := e.getKeyOperationConnection(c)
858-
if err != nil {
859-
log.Errorf("failed to get key operation connection: %s", err)
860-
} else {
861-
defer func() {
862-
if keyConn != c {
863-
keyConn.Close()
864-
}
865-
}()
866-
867+
if keyConn != nil {
867868
if err := e.extractCheckKeyMetrics(ch, keyConn); err != nil {
868869
log.Errorf("extractCheckKeyMetrics() err: %s", err)
869870
}
870-
871871
e.extractCountKeysMetrics(ch, keyConn)
872-
873872
e.extractStreamMetrics(ch, keyConn)
874873
}
875874
} else {
@@ -878,17 +877,8 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error {
878877

879878
e.extractSlowLogMetrics(ch, c)
880879

881-
// Key groups also need cluster connection for key operations
882-
keyGroupConn, err := e.getKeyOperationConnection(c)
883-
if err != nil {
884-
log.Errorf("failed to get key operation connection for key groups: %s", err)
885-
} else {
886-
defer func() {
887-
if keyGroupConn != c {
888-
keyGroupConn.Close()
889-
}
890-
}()
891-
e.extractKeyGroupMetrics(ch, keyGroupConn, dbCount)
880+
if keyConn != nil {
881+
e.extractKeyGroupMetrics(ch, keyConn, dbCount)
892882
}
893883

894884
if strings.Contains(infoAll, "# Sentinel") {

exporter/keys.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ func (e *Exporter) getKeyInfo(ch chan<- prometheus.Metric, c redis.Conn, dbLabel
4444

4545
switch keyType {
4646
case "none":
47-
log.Debugf("Key '%s' not found when trying to get type and size: using default '0.0'", keyName)
48-
e.registerConstMetricGauge(ch, "key_size", 0.0, dbLabel, keyName)
47+
log.Debugf("Key '%s' not found, skipping", keyName)
4948
return
5049

5150
case "string":

exporter/latency.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"regexp"
55
"strconv"
66
"strings"
7-
87
"sync"
98

109
"github.com/gomodule/redigo/redis"
@@ -63,7 +62,7 @@ func (e *Exporter) extractLatencyHistogramMetrics(outChan chan<- prometheus.Metr
6362
return
6463
}
6564

66-
for i := 0; i < len(reply); i += 2 {
65+
for i := 0; i+1 < len(reply); i += 2 {
6766
cmd, _ := redis.String(reply[i], nil)
6867
details, _ := redis.Values(reply[i+1], nil)
6968

@@ -76,7 +75,7 @@ func (e *Exporter) extractLatencyHistogramMetrics(outChan chan<- prometheus.Metr
7675

7776
buckets := map[float64]uint64{}
7877

79-
for j := 0; j < len(bucketInfo); j += 2 {
78+
for j := 0; j+1 < len(bucketInfo); j += 2 {
8079
usec := float64(bucketInfo[j])
8180
count := bucketInfo[j+1]
8281
buckets[usec] = count
@@ -94,7 +93,7 @@ func extractTotalUsecForCommand(infoAll string, cmd string) uint64 {
9493

9594
matches := extractUsecRegexp.FindAllStringSubmatch(infoAll, -1)
9695
for _, match := range matches {
97-
if !strings.HasPrefix(match[1], cmd) {
96+
if match[1] != cmd && !strings.HasPrefix(match[1], cmd+"|") {
9897
continue
9998
}
10099

exporter/modules.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,20 @@ func (e *Exporter) extractModulesMetrics(ch chan<- prometheus.Metric, c redis.Co
3030
if len(module) != 7 {
3131
continue
3232
}
33+
extractModuleVal := func(s string) string {
34+
parts := strings.SplitN(s, "=", 2)
35+
if len(parts) != 2 {
36+
return ""
37+
}
38+
return parts[1]
39+
}
3340
e.registerConstMetricGauge(ch, "module_info", 1,
34-
strings.Split(module[0], "=")[1],
35-
strings.Split(module[1], "=")[1],
36-
strings.Split(module[2], "=")[1],
37-
strings.Split(module[3], "=")[1],
38-
strings.Split(module[4], "=")[1],
39-
strings.Split(module[5], "=")[1],
41+
extractModuleVal(module[0]),
42+
extractModuleVal(module[1]),
43+
extractModuleVal(module[2]),
44+
extractModuleVal(module[3]),
45+
extractModuleVal(module[4]),
46+
extractModuleVal(module[5]),
4047
)
4148
continue
4249
}

exporter/sentinels.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
log "github.com/sirupsen/logrus"
1111
)
1212

13+
var reSentinelMaster = regexp.MustCompile(`^master\d+`)
14+
1315
func (e *Exporter) handleMetricsSentinel(ch chan<- prometheus.Metric, fieldKey string, fieldValue string) {
1416
switch fieldKey {
1517
case
@@ -197,7 +199,7 @@ valid examples:
197199
*/
198200
func parseSentinelMasterString(master string, masterInfo string) (masterName string, masterStatus string, masterAddr string, masterSlaves float64, masterSentinels float64, ok bool) {
199201
ok = false
200-
if matched, _ := regexp.MatchString(`^master\d+`, master); !matched {
202+
if !reSentinelMaster.MatchString(master) {
201203
return
202204
}
203205
matchedMasterInfo := make(map[string]string)

exporter/slowlog.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ func (e *Exporter) extractSlowLogMetrics(ch chan<- prometheus.Metric, c redis.Co
2020

2121
if len(values) > 0 {
2222
if values, err = redis.Values(values[0], err); err == nil && len(values) > 0 {
23-
slowlogLastID = values[0].(int64)
23+
if id, ok := values[0].(int64); ok {
24+
slowlogLastID = id
25+
}
2426
if len(values) > 2 {
25-
lastSlowExecutionDurationSeconds = float64(values[2].(int64)) / 1e6
27+
if dur, ok := values[2].(int64); ok {
28+
lastSlowExecutionDurationSeconds = float64(dur) / 1e6
29+
}
2630
}
2731
}
2832
}

exporter/streams.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,18 @@ func getStreamInfo(c redis.Conn, key string) (*streamInfo, error) {
7575
}
7676

7777
func getStreamEntryId(redisValue []interface{}, index int) string {
78-
if values, ok := redisValue[index].([]interface{}); !ok || len(values) < 2 {
78+
if index >= len(redisValue) || redisValue[index] == nil {
7979
log.Debugf("Failed to parse StreamEntryId")
8080
return ""
8181
}
8282

83-
if len(redisValue) < index || redisValue[index] == nil {
83+
values, ok := redisValue[index].([]interface{})
84+
if !ok || len(values) < 1 {
8485
log.Debugf("Failed to parse StreamEntryId")
8586
return ""
8687
}
8788

88-
entryId, ok := redisValue[index].([]interface{})[0].([]byte)
89+
entryId, ok := values[0].([]byte)
8990
if !ok {
9091
log.Debugf("Failed to parse StreamEntryId")
9192
return ""

exporter/tile38.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func (e *Exporter) extractTile38Metrics(ch chan<- prometheus.Metric, c redis.Con
1515
return
1616
}
1717

18-
for i := 0; i < len(info); i += 2 {
18+
for i := 0; i+1 < len(info); i += 2 {
1919
fieldKey := info[i]
2020
if !strings.HasPrefix(fieldKey, "tile38_") {
2121
fieldKey = "tile38_" + fieldKey

0 commit comments

Comments
 (0)