diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index b0a41447e..b394c907f 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -163,6 +163,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/ravendb" _ "github.com/influxdata/telegraf/plugins/inputs/redfish" _ "github.com/influxdata/telegraf/plugins/inputs/redis" + _ "github.com/influxdata/telegraf/plugins/inputs/redis_sentinel" _ "github.com/influxdata/telegraf/plugins/inputs/rethinkdb" _ "github.com/influxdata/telegraf/plugins/inputs/riak" _ "github.com/influxdata/telegraf/plugins/inputs/riemann_listener" diff --git a/plugins/inputs/redis_sentinel/README.md b/plugins/inputs/redis_sentinel/README.md new file mode 100644 index 000000000..777e57a2d --- /dev/null +++ b/plugins/inputs/redis_sentinel/README.md @@ -0,0 +1,206 @@ +# Redis Sentinel Input Plugin + +A plugin for Redis Sentinel to monitor multiple Sentinel instances that are +monitoring multiple Redis servers and replicas. + +## Configuration + +```toml +# Read Redis Sentinel's basic status information +[[inputs.redis_sentinel]] + ## specify servers via a url matching: + ## [protocol://][:password]@address[:port] + ## e.g. + ## tcp://localhost:26379 + ## tcp://:password@192.168.99.100 + ## + ## If no servers are specified, then localhost is used as the host. + ## If no port is specified, 26379 is used + # servers = ["tcp://localhost:26379"] + + ## Optional TLS Config + # tls_ca = "/etc/telegraf/ca.pem" + # tls_cert = "/etc/telegraf/cert.pem" + # tls_key = "/etc/telegraf/key.pem" + ## Use TLS but skip chain & host verification + # insecure_skip_verify = true +``` + +## Measurements & Fields + +The plugin gathers the results of these commands and measurements: + +* `sentinel masters` - `redis_sentinel_masters` +* `sentinel sentinels` - `redis_sentinels` +* `sentinel replicas` - `redis_replicas` +* `info all` - `redis_sentinel` + +The `has_quorum` field in `redis_sentinel_masters` is from calling the command `sentinels ckquorum`. + +There are 5 remote network requests made for each server listed in the config. + +## Metrics + +* redis_sentinel_masters + * tags: + * host + * master + * port + * source + + * fields: + * config_epoch (int) + * down_after_milliseconds (int) + * failover_timeout (int) + * flags (string) + * has_quorum (bool) + * info_refresh (int) + * ip (string) + * last_ok_ping_reply (int) + * last_ping_reply (int) + * last_ping_sent (int) + * link_pending_commands (int) + * link_refcount (int) + * num_other_sentinels (int) + * num_slaves (int) + * parallel_syncs (int) + * port (int) + * quorum (int) + * role_reported (string) + * role_reported_time (int) + +* redis_sentinel_sentinels + * tags: + * host + * master + * port + * sentinel_ip + * sentinel_port + * source + + * fields: + * down_after_milliseconds (int) + * flags (string) + * last_hello_message (int) + * last_ok_ping_reply (int) + * last_ping_reply (int) + * last_ping_sent (int) + * link_pending_commands (int) + * link_refcount (int) + * name (string) + * voted_leader (string) + * voted_leader_epoch (int) + +* redis_sentinel_replicas + * tags: + * host + * master + * port + * replica_ip + * replica_port + * source + + * fields: + * down_after_milliseconds (int) + * flags (string) + * info_refresh (int) + * last_ok_ping_reply (int) + * last_ping_reply (int) + * last_ping_sent (int) + * link_pending_commands (int) + * link_refcount (int) + * master_host (string) + * master_link_down_time (int) + * master_link_status (string) + * master_port (int) + * name (string) + * role_reported (string) + * role_reported_time (int) + * slave_priority (int) + * slave_repl_offset (int) + +* redis_sentinel + * tags: + * host + * port + * source + + * fields: + * active_defrag_hits (int) + * active_defrag_key_hits (int) + * active_defrag_key_misses (int) + * active_defrag_misses (int) + * blocked_clients (int) + * client_recent_max_input_buffer (int) + * client_recent_max_output_buffer (int) + * clients (int) + * evicted_keys (int) + * expired_keys (int) + * expired_stale_perc (float) + * expired_time_cap_reached_count (int) + * instantaneous_input_kbps (float) + * instantaneous_ops_per_sec (int) + * instantaneous_output_kbps (float) + * keyspace_hits (int) + * keyspace_misses (int) + * latest_fork_usec (int) + * lru_clock (int) + * migrate_cached_sockets (int) + * pubsub_channels (int) + * pubsub_patterns (int) + * redis_version (string) + * rejected_connections (int) + * sentinel_masters (int) + * sentinel_running_scripts (int) + * sentinel_scripts_queue_length (int) + * sentinel_simulate_failure_flags (int) + * sentinel_tilt (int) + * slave_expires_tracked_keys (int) + * sync_full (int) + * sync_partial_err (int) + * sync_partial_ok (int) + * total_commands_processed (int) + * total_connections_received (int) + * total_net_input_bytes (int) + * total_net_output_bytes (int) + * uptime_ns (int, nanoseconds) + * used_cpu_sys (float) + * used_cpu_sys_children (float) + * used_cpu_user (float) + * used_cpu_user_children (float) + +## Example Output + +An example of 2 Redis Sentinel instances monitoring a single master and replica. It produces: + +### redis_sentinel_masters + +```sh +redis_sentinel_masters,host=somehostname,master=mymaster,port=26380,source=localhost config_epoch=0i,down_after_milliseconds=30000i,failover_timeout=180000i,flags="master",has_quorum=1i,info_refresh=110i,ip="127.0.0.1",last_ok_ping_reply=819i,last_ping_reply=819i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,num_other_sentinels=1i,num_slaves=1i,parallel_syncs=1i,port=6379i,quorum=2i,role_reported="master",role_reported_time=311248i 1570207377000000000 + +redis_sentinel_masters,host=somehostname,master=mymaster,port=26379,source=localhost config_epoch=0i,down_after_milliseconds=30000i,failover_timeout=180000i,flags="master",has_quorum=1i,info_refresh=1650i,ip="127.0.0.1",last_ok_ping_reply=1003i,last_ping_reply=1003i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,num_other_sentinels=1i,num_slaves=1i,parallel_syncs=1i,port=6379i,quorum=2i,role_reported="master",role_reported_time=302990i 1570207377000000000 +``` + +### redis_sentinel_sentinels + +```sh +redis_sentinel_sentinels,host=somehostname,master=mymaster,port=26380,sentinel_ip=127.0.0.1,sentinel_port=26379,source=localhost down_after_milliseconds=30000i,flags="sentinel",last_hello_message=1337i,last_ok_ping_reply=566i,last_ping_reply=566i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,name="fd7444de58ecc00f2685cd89fc11ff96c72f0569",voted_leader="?",voted_leader_epoch=0i 1570207377000000000 + +redis_sentinel_sentinels,host=somehostname,master=mymaster,port=26379,sentinel_ip=127.0.0.1,sentinel_port=26380,source=localhost down_after_milliseconds=30000i,flags="sentinel",last_hello_message=1510i,last_ok_ping_reply=1004i,last_ping_reply=1004i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,name="d06519438fe1b35692cb2ea06d57833c959f9114",voted_leader="?",voted_leader_epoch=0i 1570207377000000000 +``` + +### redis_sentinel_replicas + +```sh +redis_sentinel_replicas,host=somehostname,master=mymaster,port=26379,replica_ip=127.0.0.1,replica_port=6380,source=localhost down_after_milliseconds=30000i,flags="slave",info_refresh=1651i,last_ok_ping_reply=1005i,last_ping_reply=1005i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,master_host="127.0.0.1",master_link_down_time=0i,master_link_status="ok",master_port=6379i,name="127.0.0.1:6380",role_reported="slave",role_reported_time=302983i,slave_priority=100i,slave_repl_offset=40175i 1570207377000000000 + +redis_sentinel_replicas,host=somehostname,master=mymaster,port=26380,replica_ip=127.0.0.1,replica_port=6380,source=localhost down_after_milliseconds=30000i,flags="slave",info_refresh=111i,last_ok_ping_reply=821i,last_ping_reply=821i,last_ping_sent=0i,link_pending_commands=0i,link_refcount=1i,master_host="127.0.0.1",master_link_down_time=0i,master_link_status="ok",master_port=6379i,name="127.0.0.1:6380",role_reported="slave",role_reported_time=311243i,slave_priority=100i,slave_repl_offset=40441i 1570207377000000000 +``` + +### redis_sentinel + +```sh +redis_sentinel,host=somehostname,port=26379,source=localhost active_defrag_hits=0i,active_defrag_key_hits=0i,active_defrag_key_misses=0i,active_defrag_misses=0i,blocked_clients=0i,client_recent_max_input_buffer=2i,client_recent_max_output_buffer=0i,clients=3i,evicted_keys=0i,expired_keys=0i,expired_stale_perc=0,expired_time_cap_reached_count=0i,instantaneous_input_kbps=0.01,instantaneous_ops_per_sec=0i,instantaneous_output_kbps=0,keyspace_hits=0i,keyspace_misses=0i,latest_fork_usec=0i,lru_clock=9926289i,migrate_cached_sockets=0i,pubsub_channels=0i,pubsub_patterns=0i,redis_version="5.0.5",rejected_connections=0i,sentinel_masters=1i,sentinel_running_scripts=0i,sentinel_scripts_queue_length=0i,sentinel_simulate_failure_flags=0i,sentinel_tilt=0i,slave_expires_tracked_keys=0i,sync_full=0i,sync_partial_err=0i,sync_partial_ok=0i,total_commands_processed=459i,total_connections_received=6i,total_net_input_bytes=24517i,total_net_output_bytes=14864i,uptime_ns=303000000000i,used_cpu_sys=0.404,used_cpu_sys_children=0,used_cpu_user=0.436,used_cpu_user_children=0 1570207377000000000 + +redis_sentinel,host=somehostname,port=26380,source=localhost active_defrag_hits=0i,active_defrag_key_hits=0i,active_defrag_key_misses=0i,active_defrag_misses=0i,blocked_clients=0i,client_recent_max_input_buffer=2i,client_recent_max_output_buffer=0i,clients=2i,evicted_keys=0i,expired_keys=0i,expired_stale_perc=0,expired_time_cap_reached_count=0i,instantaneous_input_kbps=0.01,instantaneous_ops_per_sec=0i,instantaneous_output_kbps=0,keyspace_hits=0i,keyspace_misses=0i,latest_fork_usec=0i,lru_clock=9926289i,migrate_cached_sockets=0i,pubsub_channels=0i,pubsub_patterns=0i,redis_version="5.0.5",rejected_connections=0i,sentinel_masters=1i,sentinel_running_scripts=0i,sentinel_scripts_queue_length=0i,sentinel_simulate_failure_flags=0i,sentinel_tilt=0i,slave_expires_tracked_keys=0i,sync_full=0i,sync_partial_err=0i,sync_partial_ok=0i,total_commands_processed=442i,total_connections_received=2i,total_net_input_bytes=23861i,total_net_output_bytes=4443i,uptime_ns=312000000000i,used_cpu_sys=0.46,used_cpu_sys_children=0,used_cpu_user=0.416,used_cpu_user_children=0 1570207377000000000 +``` diff --git a/plugins/inputs/redis_sentinel/redis_sentinel.go b/plugins/inputs/redis_sentinel/redis_sentinel.go new file mode 100644 index 000000000..8e627d032 --- /dev/null +++ b/plugins/inputs/redis_sentinel/redis_sentinel.go @@ -0,0 +1,455 @@ +package redis_sentinel + +import ( + "bufio" + "fmt" + "io" + "net/url" + "strconv" + "strings" + "sync" + + "github.com/go-redis/redis" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/common/tls" + "github.com/influxdata/telegraf/plugins/inputs" +) + +type RedisSentinel struct { + Servers []string `toml:"servers"` + tls.ClientConfig + + clients []*RedisSentinelClient +} + +type RedisSentinelClient struct { + sentinel *redis.SentinelClient + tags map[string]string +} + +const measurementMasters = "redis_sentinel_masters" +const measurementSentinel = "redis_sentinel" +const measurementSentinels = "redis_sentinel_sentinels" +const measurementReplicas = "redis_sentinel_replicas" + +func init() { + inputs.Add("redis_sentinel", func() telegraf.Input { + return &RedisSentinel{} + }) +} + +func (r *RedisSentinel) SampleConfig() string { + return ` + ## specify servers via a url matching: + ## [protocol://][:password]@address[:port] + ## e.g. + ## tcp://localhost:26379 + ## tcp://:password@192.168.99.100 + ## unix:///var/run/redis-sentinel.sock + ## + ## If no servers are specified, then localhost is used as the host. + ## If no port is specified, 26379 is used + # servers = ["tcp://localhost:26379"] + + ## Optional TLS Config + # tls_ca = "/etc/telegraf/ca.pem" + # tls_cert = "/etc/telegraf/cert.pem" + # tls_key = "/etc/telegraf/key.pem" + ## Use TLS but skip chain & host verification + # insecure_skip_verify = true +` +} + +func (r *RedisSentinel) Description() string { + return "Read metrics from one or many redis-sentinel servers" +} + +func (r *RedisSentinel) Init() error { + if len(r.Servers) == 0 { + r.Servers = []string{"tcp://localhost:26379"} + } + + r.clients = make([]*RedisSentinelClient, len(r.Servers)) + + tlsConfig, err := r.ClientConfig.TLSConfig() + if err != nil { + return err + } + + for i, serv := range r.Servers { + u, err := url.Parse(serv) + if err != nil { + return fmt.Errorf("unable to parse to address %q: %v", serv, err) + } + + password := "" + if u.User != nil { + password, _ = u.User.Password() + } + + var address string + tags := map[string]string{} + + switch u.Scheme { + case "tcp": + address = u.Host + tags["source"] = u.Hostname() + tags["port"] = u.Port() + case "unix": + address = u.Path + tags["socket"] = u.Path + default: + return fmt.Errorf("invalid scheme %q, expected tcp or unix", u.Scheme) + } + + sentinel := redis.NewSentinelClient( + &redis.Options{ + Addr: address, + Password: password, + Network: u.Scheme, + PoolSize: 1, + TLSConfig: tlsConfig, + }, + ) + + r.clients[i] = &RedisSentinelClient{ + sentinel: sentinel, + tags: tags, + } + } + + return nil +} + +// Redis list format has string key/values adjacent, so convert to a map for easier use +func toMap(vals []interface{}) map[string]string { + m := make(map[string]string) + + for idx := 0; idx < len(vals)-1; idx += 2 { + key, keyOk := vals[idx].(string) + value, valueOk := vals[idx+1].(string) + + if keyOk && valueOk { + m[key] = value + } + } + + return m +} + +func castFieldValue(value string, fieldType configFieldType) (interface{}, error) { + var castedValue interface{} + var err error + + switch fieldType { + case configFieldTypeFloat: + castedValue, err = strconv.ParseFloat(value, 64) + case configFieldTypeInteger: + castedValue, err = strconv.ParseInt(value, 10, 64) + case configFieldTypeString: + castedValue = value + default: + return nil, fmt.Errorf("unsupported field type %v", fieldType) + } + + if err != nil { + return nil, fmt.Errorf("casting value %v failed: %v", value, err) + } + + return castedValue, nil +} + +func prepareFieldValues(fields map[string]string, typeMap map[string]configFieldType) (map[string]interface{}, error) { + preparedFields := make(map[string]interface{}) + + for key, val := range fields { + key = strings.Replace(key, "-", "_", -1) + + valType, ok := typeMap[key] + if !ok { + continue + } + + castedVal, err := castFieldValue(val, valType) + if err != nil { + return nil, err + } + + preparedFields[key] = castedVal + } + + return preparedFields, nil +} + +// Reads stats from all configured servers accumulates stats. +// Returns one of the errors encountered while gather stats (if any). +func (r *RedisSentinel) Gather(acc telegraf.Accumulator) error { + var wg sync.WaitGroup + + for _, client := range r.clients { + wg.Add(1) + + go func(acc telegraf.Accumulator, client *RedisSentinelClient) { + defer wg.Done() + + masters, err := client.gatherMasterStats(acc) + acc.AddError(err) + + for _, master := range masters { + acc.AddError(client.gatherReplicaStats(acc, master)) + acc.AddError(client.gatherSentinelStats(acc, master)) + } + + acc.AddError(client.gatherInfoStats(acc)) + }(acc, client) + } + + wg.Wait() + + return nil +} + +func (client *RedisSentinelClient) gatherInfoStats(acc telegraf.Accumulator) error { + infoCmd := redis.NewStringCmd("info", "all") + if err := client.sentinel.Process(infoCmd); err != nil { + return err + } + + info, err := infoCmd.Result() + if err != nil { + return err + } + + rdr := strings.NewReader(info) + infoTags, infoFields, err := convertSentinelInfoOutput(client.tags, rdr) + if err != nil { + return err + } + + acc.AddFields(measurementSentinel, infoFields, infoTags) + + return nil +} + +func (client *RedisSentinelClient) gatherMasterStats(acc telegraf.Accumulator) ([]string, error) { + var masterNames []string + + mastersCmd := redis.NewSliceCmd("sentinel", "masters") + if err := client.sentinel.Process(mastersCmd); err != nil { + return masterNames, err + } + + masters, err := mastersCmd.Result() + if err != nil { + return masterNames, err + } + + // Break out of the loop if one of the items comes out malformed + // It's safe to assume that if we fail parsing one item that the rest will fail too + // This is because we are iterating over a single server response + for _, master := range masters { + master, ok := master.([]interface{}) + if !ok { + return masterNames, fmt.Errorf("unable to process master response") + } + + m := toMap(master) + + masterName, ok := m["name"] + if !ok { + return masterNames, fmt.Errorf("unable to resolve master name") + } + + quorumCmd := redis.NewStringCmd("sentinel", "ckquorum", masterName) + quorumErr := client.sentinel.Process(quorumCmd) + + sentinelMastersTags, sentinelMastersFields, err := convertSentinelMastersOutput(client.tags, m, quorumErr) + if err != nil { + return masterNames, err + } + acc.AddFields(measurementMasters, sentinelMastersFields, sentinelMastersTags) + } + + return masterNames, nil +} + +func (client *RedisSentinelClient) gatherReplicaStats(acc telegraf.Accumulator, masterName string) error { + replicasCmd := redis.NewSliceCmd("sentinel", "replicas", masterName) + if err := client.sentinel.Process(replicasCmd); err != nil { + return err + } + + replicas, err := replicasCmd.Result() + if err != nil { + return err + } + + // Break out of the loop if one of the items comes out malformed + // It's safe to assume that if we fail parsing one item that the rest will fail too + // This is because we are iterating over a single server response + for _, replica := range replicas { + replica, ok := replica.([]interface{}) + if !ok { + return fmt.Errorf("unable to process replica response") + } + + rm := toMap(replica) + replicaTags, replicaFields, err := convertSentinelReplicaOutput(client.tags, masterName, rm) + if err != nil { + return err + } + + acc.AddFields(measurementReplicas, replicaFields, replicaTags) + } + + return nil +} + +func (client *RedisSentinelClient) gatherSentinelStats(acc telegraf.Accumulator, masterName string) error { + sentinelsCmd := redis.NewSliceCmd("sentinel", "sentinels", masterName) + if err := client.sentinel.Process(sentinelsCmd); err != nil { + return err + } + + sentinels, err := sentinelsCmd.Result() + if err != nil { + return err + } + + // Break out of the loop if one of the items comes out malformed + // It's safe to assume that if we fail parsing one item that the rest will fail too + // This is because we are iterating over a single server response + for _, sentinel := range sentinels { + sentinel, ok := sentinel.([]interface{}) + if !ok { + return fmt.Errorf("unable to process sentinel response") + } + + sm := toMap(sentinel) + sentinelTags, sentinelFields, err := convertSentinelSentinelsOutput(client.tags, masterName, sm) + if err != nil { + return err + } + + acc.AddFields(measurementSentinels, sentinelFields, sentinelTags) + } + + return nil +} + +// converts `sentinel masters ` output to tags and fields +func convertSentinelMastersOutput( + globalTags map[string]string, + master map[string]string, + quorumErr error, +) (map[string]string, map[string]interface{}, error) { + tags := globalTags + + tags["master"] = master["name"] + + fields, err := prepareFieldValues(master, measurementMastersFields) + if err != nil { + return nil, nil, err + } + + fields["has_quorum"] = quorumErr == nil + + return tags, fields, nil +} + +// converts `sentinel sentinels ` output to tags and fields +func convertSentinelSentinelsOutput( + globalTags map[string]string, + masterName string, + sentinelMaster map[string]string, +) (map[string]string, map[string]interface{}, error) { + tags := globalTags + + tags["sentinel_ip"] = sentinelMaster["ip"] + tags["sentinel_port"] = sentinelMaster["port"] + tags["master"] = masterName + + fields, err := prepareFieldValues(sentinelMaster, measurementSentinelsFields) + if err != nil { + return nil, nil, err + } + + return tags, fields, nil +} + +// converts `sentinel replicas ` output to tags and fields +func convertSentinelReplicaOutput( + globalTags map[string]string, + masterName string, + replica map[string]string, +) (map[string]string, map[string]interface{}, error) { + tags := globalTags + + tags["replica_ip"] = replica["ip"] + tags["replica_port"] = replica["port"] + tags["master"] = masterName + + fields, err := prepareFieldValues(replica, measurementReplicasFields) + if err != nil { + return nil, nil, err + } + + return tags, fields, nil +} + +// convertSentinelInfoOutput parses `INFO` command output +// Largely copied from the Redis input plugin's gatherInfoOutput() +func convertSentinelInfoOutput( + globalTags map[string]string, + rdr io.Reader, +) (map[string]string, map[string]interface{}, error) { + scanner := bufio.NewScanner(rdr) + rawFields := make(map[string]string) + + tags := globalTags + + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + + // Redis denotes configuration sections with a hashtag + // Example of the section header: # Clients + if line[0] == '#' { + // Nothing interesting here + continue + } + + parts := strings.SplitN(line, ":", 2) + if len(parts) < 2 { + // Not a valid configuration option + continue + } + + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + + rawFields[key] = val + } + + fields, err := prepareFieldValues(rawFields, measurementSentinelFields) + if err != nil { + return nil, nil, err + } + + // Rename the field and convert it to nanoseconds + secs, ok := fields["uptime_in_seconds"].(int64) + if !ok { + return nil, nil, fmt.Errorf("uptime type %T is not int64", fields["uptime_in_seconds"]) + } + fields["uptime_ns"] = secs * 1000_000_000 + delete(fields, "uptime_in_seconds") + + // Rename in order to match the "redis" input plugin + fields["clients"] = fields["connected_clients"] + delete(fields, "connected_clients") + + return tags, fields, nil +} diff --git a/plugins/inputs/redis_sentinel/redis_sentinel_test.go b/plugins/inputs/redis_sentinel/redis_sentinel_test.go new file mode 100644 index 000000000..0cc8c1551 --- /dev/null +++ b/plugins/inputs/redis_sentinel/redis_sentinel_test.go @@ -0,0 +1,311 @@ +package redis_sentinel + +import ( + "bufio" + "bytes" + "fmt" + "os" + "testing" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/testutil" + + "github.com/stretchr/testify/require" +) + +const masterName = "mymaster" + +func TestRedisSentinelConnect(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + addr := fmt.Sprintf("tcp://" + testutil.GetLocalHost() + ":26379") + + r := &RedisSentinel{ + Servers: []string{addr}, + } + + var acc testutil.Accumulator + + err := acc.GatherError(r.Gather) + require.NoError(t, err) +} + +func TestRedisSentinelMasters(t *testing.T) { + now := time.Now() + + globalTags := map[string]string{ + "port": "6379", + "source": "redis.io", + } + + expectedTags := map[string]string{ + "port": "6379", + "source": "redis.io", + "master": masterName, + } + + // has_quorum is a custom field + expectedFields := map[string]interface{}{ + "config_epoch": 0, + "down_after_milliseconds": 30000, + "failover_timeout": 180000, + "flags": "master", + "info_refresh": 8819, + "ip": "127.0.0.1", + "last_ok_ping_reply": 174, + "last_ping_reply": 174, + "last_ping_sent": 0, + "link_pending_commands": 0, + "link_refcount": 1, + "num_other_sentinels": 1, + "num_slaves": 0, + "parallel_syncs": 1, + "port": 6379, + "quorum": 2, + "role_reported": "master", + "role_reported_time": 83138826, + "has_quorum": true, + } + + expectedMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementMasters, expectedTags, expectedFields, now), + } + + sentinelMastersOutput := map[string]string{ + "config_epoch": "0", + "down_after_milliseconds": "30000", + "failover_timeout": "180000", + "flags": "master", + "info_refresh": "8819", + "ip": "127.0.0.1", + "last_ok_ping_reply": "174", + "last_ping_reply": "174", + "last_ping_sent": "0", + "link_pending_commands": "0", + "link_refcount": "1", + "name": "mymaster", + "num_other_sentinels": "1", + "num_slaves": "0", + "parallel_syncs": "1", + "port": "6379", + "quorum": "2", + "role_reported": "master", + "role_reported_time": "83138826", + "runid": "ff3dadd1cfea3043de4d25711d93f01a564562f7", + } + + sentinelTags, sentinelFields, sentinalErr := convertSentinelMastersOutput(globalTags, sentinelMastersOutput, nil) + require.NoErrorf(t, sentinalErr, "failed converting output: %v", sentinalErr) + + actualMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementMasters, sentinelTags, sentinelFields, now), + } + + testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics, testutil.IgnoreTime()) +} + +func TestRedisSentinels(t *testing.T) { + now := time.Now() + + globalTags := make(map[string]string) + + expectedTags := map[string]string{ + "sentinel_ip": "127.0.0.1", + "sentinel_port": "26380", + "master": masterName, + } + expectedFields := map[string]interface{}{ + "name": "adfd343f6b6ecc77e2b9636de6d9f28d4b827521", + "flags": "sentinel", + "link_pending_commands": 0, + "link_refcount": 1, + "last_ping_sent": 0, + "last_ok_ping_reply": 516, + "last_ping_reply": 516, + "down_after_milliseconds": 30000, + "last_hello_message": 1905, + "voted_leader": "?", + "voted_leader_epoch": 0, + } + + expectedMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementSentinels, expectedTags, expectedFields, now), + } + + sentinelsOutput := map[string]string{ + "name": "adfd343f6b6ecc77e2b9636de6d9f28d4b827521", + "ip": "127.0.0.1", + "port": "26380", + "runid": "adfd343f6b6ecc77e2b9636de6d9f28d4b827521", + "flags": "sentinel", + "link_pending_commands": "0", + "link_refcount": "1", + "last_ping_sent": "0", + "last_ok_ping_reply": "516", + "last_ping_reply": "516", + "down_after_milliseconds": "30000", + "last_hello_message": "1905", + "voted_leader": "?", + "voted_leader_epoch": "0", + } + + sentinelTags, sentinelFields, sentinelErr := convertSentinelSentinelsOutput(globalTags, masterName, sentinelsOutput) + require.NoErrorf(t, sentinelErr, "failed converting output: %v", sentinelErr) + + actualMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementSentinels, sentinelTags, sentinelFields, now), + } + + testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics) +} + +func TestRedisSentinelReplicas(t *testing.T) { + now := time.Now() + + globalTags := make(map[string]string) + + expectedTags := map[string]string{ + "replica_ip": "127.0.0.1", + "replica_port": "6380", + "master": masterName, + } + expectedFields := map[string]interface{}{ + "down_after_milliseconds": 30000, + "flags": "slave", + "info_refresh": 8476, + "last_ok_ping_reply": 987, + "last_ping_reply": 987, + "last_ping_sent": 0, + "link_pending_commands": 0, + "link_refcount": 1, + "master_host": "127.0.0.1", + "master_link_down_time": 0, + "master_link_status": "ok", + "master_port": 6379, + "name": "127.0.0.1:6380", + "role_reported": "slave", + "role_reported_time": 10267432, + "slave_priority": 100, + "slave_repl_offset": 1392400, + } + + expectedMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementReplicas, expectedTags, expectedFields, now), + } + + replicasOutput := map[string]string{ + "down_after_milliseconds": "30000", + "flags": "slave", + "info_refresh": "8476", + "ip": "127.0.0.1", + "last_ok_ping_reply": "987", + "last_ping_reply": "987", + "last_ping_sent": "0", + "link_pending_commands": "0", + "link_refcount": "1", + "master_host": "127.0.0.1", + "master_link_down_time": "0", + "master_link_status": "ok", + "master_port": "6379", + "name": "127.0.0.1:6380", + "port": "6380", + "role_reported": "slave", + "role_reported_time": "10267432", + "runid": "70e07dad9e450e2d35f1b75338e0a5341b59d710", + "slave_priority": "100", + "slave_repl_offset": "1392400", + } + + sentinelTags, sentinelFields, sentinalErr := convertSentinelReplicaOutput(globalTags, masterName, replicasOutput) + require.NoErrorf(t, sentinalErr, "failed converting output: %v", sentinalErr) + + actualMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementReplicas, sentinelTags, sentinelFields, now), + } + + testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics) +} + +func TestRedisSentinelInfoAll(t *testing.T) { + now := time.Now() + + globalTags := map[string]string{ + "port": "6379", + "source": "redis.io", + } + + expectedTags := map[string]string{ + "port": "6379", + "source": "redis.io", + } + + expectedFields := map[string]interface{}{ + "lru_clock": int64(15585808), + "uptime_ns": int64(901000000000), + "redis_version": "5.0.5", + + "clients": int64(2), + "client_recent_max_input_buffer": int64(2), + "client_recent_max_output_buffer": int64(0), + "blocked_clients": int64(0), + + "used_cpu_sys": float64(0.786872), + "used_cpu_user": float64(0.939455), + "used_cpu_sys_children": float64(0.000000), + "used_cpu_user_children": float64(0.000000), + + "total_connections_received": int64(2), + "total_commands_processed": int64(6), + "instantaneous_ops_per_sec": int64(0), + "total_net_input_bytes": int64(124), + "total_net_output_bytes": int64(10148), + "instantaneous_input_kbps": float64(0.00), + "instantaneous_output_kbps": float64(0.00), + "rejected_connections": int64(0), + "sync_full": int64(0), + "sync_partial_ok": int64(0), + "sync_partial_err": int64(0), + "expired_keys": int64(0), + "expired_stale_perc": float64(0.00), + "expired_time_cap_reached_count": int64(0), + "evicted_keys": int64(0), + "keyspace_hits": int64(0), + "keyspace_misses": int64(0), + "pubsub_channels": int64(0), + "pubsub_patterns": int64(0), + "latest_fork_usec": int64(0), + "migrate_cached_sockets": int64(0), + "slave_expires_tracked_keys": int64(0), + "active_defrag_hits": int64(0), + "active_defrag_misses": int64(0), + "active_defrag_key_hits": int64(0), + "active_defrag_key_misses": int64(0), + + "sentinel_masters": int64(2), + "sentinel_running_scripts": int64(0), + "sentinel_scripts_queue_length": int64(0), + "sentinel_simulate_failure_flags": int64(0), + "sentinel_tilt": int64(0), + } + + expectedMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementSentinel, expectedTags, expectedFields, now), + } + + sentinelInfoResponse, err := os.ReadFile("testdata/sentinel.info.response") + require.NoErrorf(t, err, "could not init fixture: %v", err) + + rdr := bufio.NewReader(bytes.NewReader(sentinelInfoResponse)) + + sentinelTags, sentinelFields, sentinalErr := convertSentinelInfoOutput(globalTags, rdr) + require.NoErrorf(t, sentinalErr, "failed converting output: %v", sentinalErr) + + actualMetrics := []telegraf.Metric{ + testutil.MustMetric(measurementSentinel, sentinelTags, sentinelFields, now), + } + + testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics) +} diff --git a/plugins/inputs/redis_sentinel/redis_sentinel_types.go b/plugins/inputs/redis_sentinel/redis_sentinel_types.go new file mode 100644 index 000000000..1f626c712 --- /dev/null +++ b/plugins/inputs/redis_sentinel/redis_sentinel_types.go @@ -0,0 +1,113 @@ +package redis_sentinel + +type configFieldType int32 + +const ( + configFieldTypeInteger configFieldType = iota + configFieldTypeString + configFieldTypeFloat +) + +// Supported fields for "redis_sentinel_masters" +var measurementMastersFields = map[string]configFieldType{ + "config_epoch": configFieldTypeInteger, + "down_after_milliseconds": configFieldTypeInteger, + "failover_timeout": configFieldTypeInteger, + "flags": configFieldTypeString, + "info_refresh": configFieldTypeInteger, + "ip": configFieldTypeString, + "last_ok_ping_reply": configFieldTypeInteger, + "last_ping_reply": configFieldTypeInteger, + "last_ping_sent": configFieldTypeInteger, + "link_pending_commands": configFieldTypeInteger, + "link_refcount": configFieldTypeInteger, + "num_other_sentinels": configFieldTypeInteger, + "num_slaves": configFieldTypeInteger, + "parallel_syncs": configFieldTypeInteger, + "port": configFieldTypeInteger, + "quorum": configFieldTypeInteger, + "role_reported": configFieldTypeString, + "role_reported_time": configFieldTypeInteger, +} + +// Supported fields for "redis_sentinel" +var measurementSentinelFields = map[string]configFieldType{ + "active_defrag_hits": configFieldTypeInteger, + "active_defrag_key_hits": configFieldTypeInteger, + "active_defrag_key_misses": configFieldTypeInteger, + "active_defrag_misses": configFieldTypeInteger, + "blocked_clients": configFieldTypeInteger, + "client_recent_max_input_buffer": configFieldTypeInteger, + "client_recent_max_output_buffer": configFieldTypeInteger, + "connected_clients": configFieldTypeInteger, // Renamed to "clients" + "evicted_keys": configFieldTypeInteger, + "expired_keys": configFieldTypeInteger, + "expired_stale_perc": configFieldTypeFloat, + "expired_time_cap_reached_count": configFieldTypeInteger, + "instantaneous_input_kbps": configFieldTypeFloat, + "instantaneous_ops_per_sec": configFieldTypeInteger, + "instantaneous_output_kbps": configFieldTypeFloat, + "keyspace_hits": configFieldTypeInteger, + "keyspace_misses": configFieldTypeInteger, + "latest_fork_usec": configFieldTypeInteger, + "lru_clock": configFieldTypeInteger, + "migrate_cached_sockets": configFieldTypeInteger, + "pubsub_channels": configFieldTypeInteger, + "pubsub_patterns": configFieldTypeInteger, + "redis_version": configFieldTypeString, + "rejected_connections": configFieldTypeInteger, + "sentinel_masters": configFieldTypeInteger, + "sentinel_running_scripts": configFieldTypeInteger, + "sentinel_scripts_queue_length": configFieldTypeInteger, + "sentinel_simulate_failure_flags": configFieldTypeInteger, + "sentinel_tilt": configFieldTypeInteger, + "slave_expires_tracked_keys": configFieldTypeInteger, + "sync_full": configFieldTypeInteger, + "sync_partial_err": configFieldTypeInteger, + "sync_partial_ok": configFieldTypeInteger, + "total_commands_processed": configFieldTypeInteger, + "total_connections_received": configFieldTypeInteger, + "total_net_input_bytes": configFieldTypeInteger, + "total_net_output_bytes": configFieldTypeInteger, + "uptime_in_seconds": configFieldTypeInteger, // Renamed to "uptime_ns" + "used_cpu_sys": configFieldTypeFloat, + "used_cpu_sys_children": configFieldTypeFloat, + "used_cpu_user": configFieldTypeFloat, + "used_cpu_user_children": configFieldTypeFloat, +} + +// Supported fields for "redis_sentinel_sentinels" +var measurementSentinelsFields = map[string]configFieldType{ + "down_after_milliseconds": configFieldTypeInteger, + "flags": configFieldTypeString, + "last_hello_message": configFieldTypeInteger, + "last_ok_ping_reply": configFieldTypeInteger, + "last_ping_reply": configFieldTypeInteger, + "last_ping_sent": configFieldTypeInteger, + "link_pending_commands": configFieldTypeInteger, + "link_refcount": configFieldTypeInteger, + "name": configFieldTypeString, + "voted_leader": configFieldTypeString, + "voted_leader_epoch": configFieldTypeInteger, +} + +// Supported fields for "redis_sentinel_replicas" +var measurementReplicasFields = map[string]configFieldType{ + "down_after_milliseconds": configFieldTypeInteger, + "flags": configFieldTypeString, + "info_refresh": configFieldTypeInteger, + "last_ok_ping_reply": configFieldTypeInteger, + "last_ping_reply": configFieldTypeInteger, + "last_ping_sent": configFieldTypeInteger, + "link_pending_commands": configFieldTypeInteger, + "link_refcount": configFieldTypeInteger, + "master_host": configFieldTypeString, + "master_link_down_time": configFieldTypeInteger, + "master_link_status": configFieldTypeString, + "master_port": configFieldTypeInteger, + "name": configFieldTypeString, + "role_reported": configFieldTypeString, + "role_reported_time": configFieldTypeInteger, + "slave_priority": configFieldTypeInteger, + "slave_repl_offset": configFieldTypeInteger, +} diff --git a/plugins/inputs/redis_sentinel/testdata/sentinel.info.response b/plugins/inputs/redis_sentinel/testdata/sentinel.info.response new file mode 100644 index 000000000..6915d01da --- /dev/null +++ b/plugins/inputs/redis_sentinel/testdata/sentinel.info.response @@ -0,0 +1,71 @@ +# Server +redis_version:5.0.5 +redis_git_sha1:00000000 +redis_git_dirty:0 +redis_build_id:78473e0efb96880a +redis_mode:sentinel +os:Linux 5.1.3-arch1-1-ARCH x86_64 +arch_bits:64 +multiplexing_api:epoll +atomicvar_api:atomic-builtin +gcc_version:8.3.0 +process_id:2837 +run_id:ecbbb2ca0035a532b03748fbec9f3f8ca1967536 +tcp_port:26379 +uptime_in_seconds:901 +uptime_in_days:0 +hz:10 +configured_hz:10 +lru_clock:15585808 +executable:/home/adam/redis-sentinel +config_file:/home/adam/rs1.conf + +# Clients +connected_clients:2 +client_recent_max_input_buffer:2 +client_recent_max_output_buffer:0 +blocked_clients:0 + +# CPU +used_cpu_sys:0.786872 +used_cpu_user:0.939455 +used_cpu_sys_children:0.000000 +used_cpu_user_children:0.000000 + +# Stats +total_connections_received:2 +total_commands_processed:6 +instantaneous_ops_per_sec:0 +total_net_input_bytes:124 +total_net_output_bytes:10148 +instantaneous_input_kbps:0.00 +instantaneous_output_kbps:0.00 +rejected_connections:0 +sync_full:0 +sync_partial_ok:0 +sync_partial_err:0 +expired_keys:0 +expired_stale_perc:0.00 +expired_time_cap_reached_count:0 +evicted_keys:0 +keyspace_hits:0 +keyspace_misses:0 +pubsub_channels:0 +pubsub_patterns:0 +latest_fork_usec:0 +migrate_cached_sockets:0 +slave_expires_tracked_keys:0 +active_defrag_hits:0 +active_defrag_misses:0 +active_defrag_key_hits:0 +active_defrag_key_misses:0 + +# Sentinel +sentinel_masters:2 +sentinel_tilt:0 +sentinel_running_scripts:0 +sentinel_scripts_queue_length:0 +sentinel_simulate_failure_flags:0 +master0:name=myothermaster,status=ok,address=127.0.0.1:6380,slaves=1,sentinels=2 +master0:name=myothermaster,status=ok,address=127.0.0.1:6381,slaves=1,sentinels=2 +master1:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=1,sentinels=1