feat: add Redis Sentinel input plugin (#10042)

This commit is contained in:
Petar Obradović 2021-12-14 23:13:33 +01:00 committed by GitHub
parent 91cf764eff
commit 32ca79f83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1157 additions and 0 deletions

View File

@ -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"

View File

@ -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
```

View File

@ -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 <name>` 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 <name>` 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 <name>` 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
}

View File

@ -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)
}

View File

@ -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,
}

View File

@ -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