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  }