github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-redis/lib/redis.go (about) 1 //go:build linux 2 3 package mpredis 4 5 import ( 6 "context" 7 "crypto/tls" 8 "flag" 9 "fmt" 10 "net" 11 "os" 12 "os/signal" 13 "regexp" 14 "strconv" 15 "strings" 16 "time" 17 18 mp "github.com/mackerelio/go-mackerel-plugin-helper" 19 "github.com/mackerelio/golib/logging" 20 "github.com/redis/go-redis/v9" 21 "golang.org/x/text/cases" 22 "golang.org/x/text/language" 23 ) 24 25 var logger = logging.GetLogger("metrics.plugin.redis") 26 27 // RedisPlugin mackerel plugin for Redis 28 type RedisPlugin struct { 29 ctx context.Context 30 rdb *redis.Client 31 32 Host string 33 Port string 34 Username string 35 Password string 36 Socket string 37 Prefix string 38 Timeout int 39 Tempfile string 40 ConfigCommand string 41 42 EnableTLS bool 43 InsecureSkipVerify bool 44 } 45 46 func (m *RedisPlugin) configCmd(key string) (*redis.MapStringStringCmd, error) { 47 cmd := redis.NewMapStringStringCmd(m.ctx, m.ConfigCommand, "get", key) 48 err := m.rdb.Process(m.ctx, cmd) 49 if err != nil { 50 return nil, err 51 } 52 return cmd, nil 53 } 54 55 func (m *RedisPlugin) fetchPercentageOfMemory(stat map[string]interface{}) error { 56 cmd, err := m.configCmd("maxmemory") 57 if err != nil { 58 logger.Errorf("Failed to run `%s GET maxmemory` command. %s", m.ConfigCommand, err) 59 return err 60 } 61 res, err := cmd.Result() 62 if err != nil { 63 logger.Errorf("Failed to run `%s GET maxmemory` command. %s", m.ConfigCommand, err) 64 return err 65 } 66 67 maxsize, err := strconv.ParseFloat(res["maxmemory"], 64) 68 if err != nil { 69 logger.Errorf("Failed to parse maxmemory. %s", err) 70 return err 71 } 72 73 if maxsize == 0.0 { 74 stat["percentage_of_memory"] = 0.0 75 } else { 76 stat["percentage_of_memory"] = 100.0 * stat["used_memory"].(float64) / maxsize 77 } 78 79 return nil 80 } 81 82 func (m *RedisPlugin) fetchPercentageOfClients(stat map[string]interface{}) error { 83 cmd, err := m.configCmd("maxclients") 84 if err != nil { 85 logger.Errorf("Failed to run `%s GET maxclients` command. %s", m.ConfigCommand, err) 86 return err 87 } 88 res, err := cmd.Result() 89 if err != nil { 90 logger.Errorf("Failed to run `%s GET maxclients` command. %s", m.ConfigCommand, err) 91 return err 92 } 93 94 maxsize, err := strconv.ParseFloat(res["maxclients"], 64) 95 if err != nil { 96 logger.Errorf("Failed to parse maxclients. %s", err) 97 return err 98 } 99 100 stat["percentage_of_clients"] = 100.0 * stat["connected_clients"].(float64) / maxsize 101 102 return nil 103 } 104 105 func (m *RedisPlugin) calculateCapacity(stat map[string]interface{}) error { 106 if err := m.fetchPercentageOfMemory(stat); err != nil { 107 return err 108 } 109 return m.fetchPercentageOfClients(stat) 110 } 111 112 // MetricKeyPrefix interface for PluginWithPrefix 113 func (m RedisPlugin) MetricKeyPrefix() string { 114 if m.Prefix == "" { 115 m.Prefix = "redis" 116 } 117 return m.Prefix 118 } 119 120 var ( 121 commentLine = regexp.MustCompile("^#") 122 dbLine = regexp.MustCompile("^db") 123 slaveLine = regexp.MustCompile(`^slave\d+`) 124 ) 125 126 func (m *RedisPlugin) Connect() { 127 network := "tcp" 128 address := net.JoinHostPort(m.Host, m.Port) 129 if m.Socket != "" { 130 network = "unix" 131 address = m.Socket 132 } 133 options := &redis.Options{ 134 Addr: address, 135 Username: m.Username, 136 Password: m.Password, 137 DB: 0, 138 Network: network, 139 DialTimeout: time.Duration(m.Timeout) * time.Second, 140 } 141 if m.EnableTLS { 142 options.TLSConfig = &tls.Config{ 143 InsecureSkipVerify: m.InsecureSkipVerify, 144 } 145 } 146 m.rdb = redis.NewClient(options) 147 } 148 149 // FetchMetrics interface for mackerelplugin 150 func (m RedisPlugin) FetchMetrics() (map[string]interface{}, error) { 151 str, err := m.rdb.Info(m.ctx).Result() 152 if err != nil { 153 logger.Errorf("Failed to run info command. %s", err) 154 return nil, err 155 } 156 157 stat := make(map[string]interface{}) 158 159 keysStat := 0.0 160 expiresStat := 0.0 161 var slaves []string 162 163 for _, line := range strings.Split(str, "\r\n") { 164 if line == "" { 165 continue 166 } 167 if commentLine.MatchString(line) { 168 continue 169 } 170 171 record := strings.SplitN(line, ":", 2) 172 if len(record) < 2 { 173 continue 174 } 175 key, value := record[0], record[1] 176 177 if slaveLine.MatchString(key) { 178 slaves = append(slaves, key) 179 kv := strings.Split(value, ",") 180 var offset, lag string 181 if len(kv) == 5 { 182 _, _, _, offset, lag = kv[0], kv[1], kv[2], kv[3], kv[4] 183 lagKv := strings.SplitN(lag, "=", 2) 184 lagFv, err := strconv.ParseFloat(lagKv[1], 64) 185 if err != nil { 186 logger.Warningf("Failed to parse slaves. %s", err) 187 } else { 188 stat[fmt.Sprintf("%s_lag", key)] = lagFv 189 } 190 } else { 191 _, _, _, offset = kv[0], kv[1], kv[2], kv[3] 192 } 193 offsetKv := strings.SplitN(offset, "=", 2) 194 offsetFv, err := strconv.ParseFloat(offsetKv[1], 64) 195 if err != nil { 196 logger.Warningf("Failed to parse slaves. %s", err) 197 continue 198 } 199 stat[fmt.Sprintf("%s_offset_delay", key)] = offsetFv 200 continue 201 } 202 203 if dbLine.MatchString(key) { 204 kv := strings.SplitN(value, ",", 3) 205 keys, expires := kv[0], kv[1] 206 207 keysKv := strings.SplitN(keys, "=", 2) 208 keysFv, err := strconv.ParseFloat(keysKv[1], 64) 209 if err != nil { 210 logger.Warningf("Failed to parse db keys. %s", err) 211 } else { 212 keysStat += keysFv 213 } 214 215 expiresKv := strings.SplitN(expires, "=", 2) 216 expiresFv, err := strconv.ParseFloat(expiresKv[1], 64) 217 if err != nil { 218 logger.Warningf("Failed to parse db expires. %s", err) 219 } else { 220 expiresStat += expiresFv 221 } 222 223 continue 224 } 225 226 v, err := strconv.ParseFloat(value, 64) 227 if err != nil { 228 continue 229 } 230 stat[key] = v 231 } 232 233 stat["keys"] = keysStat 234 stat["expires"] = expiresStat 235 236 if _, ok := stat["expired_keys"]; ok { 237 stat["expired"] = stat["expired_keys"] 238 } else { 239 stat["expired"] = 0.0 240 } 241 242 if m.ConfigCommand != "" { 243 if err := m.calculateCapacity(stat); err != nil { 244 logger.Infof("Failed to calculate capacity. (The cause may be that AWS Elasticache Redis has no `%s` command.) Skip these metrics. %s", m.ConfigCommand, err) 245 } 246 } 247 248 for _, slave := range slaves { 249 stat[fmt.Sprintf("%s_offset_delay", slave)] = stat["master_repl_offset"].(float64) - stat[fmt.Sprintf("%s_offset_delay", slave)].(float64) 250 } 251 252 return stat, nil 253 } 254 255 // GraphDefinition interface for mackerelplugin 256 func (m RedisPlugin) GraphDefinition() map[string]mp.Graphs { 257 labelPrefix := cases.Title(language.Und, cases.NoLower).String(m.Prefix) 258 259 var graphdef = map[string]mp.Graphs{ 260 "queries": { 261 Label: (labelPrefix + " Queries"), 262 Unit: "integer", 263 Metrics: []mp.Metrics{ 264 {Name: "total_commands_processed", Label: "Queries", Diff: true}, 265 }, 266 }, 267 "connections": { 268 Label: (labelPrefix + " Connections"), 269 Unit: "integer", 270 Metrics: []mp.Metrics{ 271 {Name: "total_connections_received", Label: "Connections", Diff: true, Stacked: true}, 272 {Name: "rejected_connections", Label: "Rejected Connections", Diff: true, Stacked: true}, 273 }, 274 }, 275 "clients": { 276 Label: (labelPrefix + " Clients"), 277 Unit: "integer", 278 Metrics: []mp.Metrics{ 279 {Name: "connected_clients", Label: "Connected Clients", Diff: false, Stacked: true}, 280 {Name: "blocked_clients", Label: "Blocked Clients", Diff: false, Stacked: true}, 281 {Name: "connected_slaves", Label: "Connected Slaves", Diff: false, Stacked: true}, 282 }, 283 }, 284 "keys": { 285 Label: (labelPrefix + " Keys"), 286 Unit: "integer", 287 Metrics: []mp.Metrics{ 288 {Name: "keys", Label: "Keys", Diff: false}, 289 {Name: "expires", Label: "Keys with expiration", Diff: false}, 290 {Name: "expired", Label: "Expired Keys", Diff: true}, 291 {Name: "evicted_keys", Label: "Evicted Keys", Diff: true}, 292 }, 293 }, 294 "keyspace": { 295 Label: (labelPrefix + " Keyspace"), 296 Unit: "integer", 297 Metrics: []mp.Metrics{ 298 {Name: "keyspace_hits", Label: "Keyspace Hits", Diff: true}, 299 {Name: "keyspace_misses", Label: "Keyspace Missed", Diff: true}, 300 }, 301 }, 302 "memory": { 303 Label: (labelPrefix + " Memory"), 304 Unit: "integer", 305 Metrics: []mp.Metrics{ 306 {Name: "used_memory", Label: "Used Memory", Diff: false}, 307 {Name: "used_memory_rss", Label: "Used Memory RSS", Diff: false}, 308 {Name: "used_memory_peak", Label: "Used Memory Peak", Diff: false}, 309 {Name: "used_memory_lua", Label: "Used Memory Lua engine", Diff: false}, 310 }, 311 }, 312 "capacity": { 313 Label: (labelPrefix + " Capacity"), 314 Unit: "percentage", 315 Metrics: []mp.Metrics{ 316 {Name: "percentage_of_memory", Label: "Percentage of memory", Diff: false}, 317 {Name: "percentage_of_clients", Label: "Percentage of clients", Diff: false}, 318 }, 319 }, 320 "uptime": { 321 Label: (labelPrefix + " Uptime"), 322 Unit: "integer", 323 Metrics: []mp.Metrics{ 324 {Name: "uptime_in_seconds", Label: "Uptime In Seconds", Diff: false}, 325 }, 326 }, 327 } 328 329 str, err := m.rdb.Info(m.ctx).Result() 330 if err != nil { 331 logger.Errorf("Failed to run info command. %s", err) 332 return nil 333 } 334 335 var metricsLag []mp.Metrics 336 var metricsOffsetDelay []mp.Metrics 337 for _, line := range strings.Split(str, "\r\n") { 338 if line == "" { 339 continue 340 } 341 342 record := strings.SplitN(line, ":", 2) 343 if len(record) < 2 { 344 continue 345 } 346 key, _ := record[0], record[1] 347 348 if slaveLine.MatchString(key) { 349 metricsLag = append(metricsLag, mp.Metrics{Name: fmt.Sprintf("%s_lag", key), Label: fmt.Sprintf("Replication lag to %s", key), Diff: false}) 350 metricsOffsetDelay = append(metricsOffsetDelay, mp.Metrics{Name: fmt.Sprintf("%s_offset_delay", key), Label: fmt.Sprintf("Offset delay to %s", key), Diff: false}) 351 } 352 } 353 354 if len(metricsLag) > 0 { 355 graphdef["lag"] = mp.Graphs{ 356 Label: (labelPrefix + " Slave Lag"), 357 Unit: "seconds", 358 Metrics: metricsLag, 359 } 360 } 361 if len(metricsOffsetDelay) > 0 { 362 graphdef["offset_delay"] = mp.Graphs{ 363 Label: (labelPrefix + " Slave Offset Delay"), 364 Unit: "count", 365 Metrics: metricsOffsetDelay, 366 } 367 } 368 369 return graphdef 370 } 371 372 // Do the plugin 373 func Do() { 374 optHost := flag.String("host", "localhost", "Hostname") 375 optPort := flag.String("port", "6379", "Port") 376 optUsername := flag.String("username", "default", "Username") 377 optPassword := flag.String("password", os.Getenv("REDIS_PASSWORD"), "Password") 378 optSocket := flag.String("socket", "", "Server socket (overrides host and port)") 379 optPrefix := flag.String("metric-key-prefix", "redis", "Metric key prefix") 380 optTimeout := flag.Int("timeout", 5, "Timeout") 381 optTempfile := flag.String("tempfile", "", "Temp file name") 382 optConfigCommand := flag.String("config-command", "CONFIG", "Custom CONFIG command. Disable CONFIG command when passed \"\".") 383 optEnableTLS := flag.Bool("tls", false, "Enables TLS connection") 384 optTLSSkipVerify := flag.Bool("tls-skip-verify", false, "Disable TLS certificate verification") 385 386 flag.Parse() 387 388 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 389 defer stop() 390 391 redis := RedisPlugin{ 392 ctx: ctx, 393 Timeout: *optTimeout, 394 Prefix: *optPrefix, 395 ConfigCommand: *optConfigCommand, 396 } 397 if *optSocket != "" { 398 redis.Socket = *optSocket 399 } else { 400 redis.Host = *optHost 401 redis.Port = *optPort 402 redis.Username = *optUsername 403 redis.Password = *optPassword 404 redis.EnableTLS = *optEnableTLS 405 redis.InsecureSkipVerify = *optTLSSkipVerify 406 } 407 redis.Connect() 408 defer redis.rdb.Close() 409 410 helper := mp.NewMackerelPlugin(redis) 411 helper.Tempfile = *optTempfile 412 413 helper.Run() 414 }