1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,214 @@
# Redis Sentinel Input Plugin
A plugin for Redis Sentinel to monitor multiple Sentinel instances that are
monitoring multiple Redis servers and replicas.
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Read metrics from one or many redis-sentinel servers
[[inputs.redis_sentinel]]
## specify servers via a url matching:
## [protocol://][username:password]@address[:port]
## e.g.
## tcp://localhost:26379
## tcp://username: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
```
## 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
```text
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
```text
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
```text
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
```text
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,436 @@
//go:generate ../../../tools/readme_config_includer/generator
package redis_sentinel
import (
"bufio"
_ "embed"
"errors"
"fmt"
"io"
"net/url"
"strconv"
"strings"
"sync"
"github.com/go-redis/redis/v7"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const (
measurementMasters = "redis_sentinel_masters"
measurementSentinel = "redis_sentinel"
measurementSentinels = "redis_sentinel_sentinels"
measurementReplicas = "redis_sentinel_replicas"
)
type RedisSentinel struct {
Servers []string `toml:"servers"`
tls.ClientConfig
clients []*redisSentinelClient
}
type redisSentinelClient struct {
sentinel *redis.SentinelClient
tags map[string]string
}
func (*RedisSentinel) SampleConfig() string {
return sampleConfig
}
func (r *RedisSentinel) Init() error {
if len(r.Servers) == 0 {
r.Servers = []string{"tcp://localhost:26379"}
}
tlsConfig, err := r.ClientConfig.TLSConfig()
if err != nil {
return err
}
r.clients = make([]*redisSentinelClient, 0, len(r.Servers))
for _, serv := range r.Servers {
u, err := url.Parse(serv)
if err != nil {
return fmt.Errorf("unable to parse to address %q: %w", serv, err)
}
username := ""
password := ""
if u.User != nil {
username = u.User.Username()
pw, ok := u.User.Password()
if ok {
password = pw
}
}
var address string
tags := make(map[string]string, 2)
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,
Username: username,
Password: password,
Network: u.Scheme,
PoolSize: 1,
TLSConfig: tlsConfig,
},
)
r.clients = append(r.clients, &redisSentinelClient{
sentinel: sentinel,
tags: tags,
})
}
return nil
}
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
}
// 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 %q failed: %w", 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.ReplaceAll(key, "-", "_")
valType, ok := typeMap[key]
if !ok {
continue
}
castedVal, err := castFieldValue(val, valType)
if err != nil {
return nil, err
}
preparedFields[key] = castedVal
}
return preparedFields, 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) {
mastersCmd := redis.NewSliceCmd("sentinel", "masters")
if err := client.sentinel.Process(mastersCmd); err != nil {
return nil, err
}
masters, err := mastersCmd.Result()
if err != nil {
return nil, 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
masterNames := make([]string, 0, len(masters))
for _, master := range masters {
master, ok := master.([]interface{})
if !ok {
return masterNames, errors.New("unable to process master response")
}
m := toMap(master)
masterName, ok := m["name"]
if !ok {
return masterNames, errors.New("unable to resolve master name")
}
masterNames = append(masterNames, masterName)
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 errors.New("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 errors.New("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, 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
}
func init() {
inputs.Add("redis_sentinel", func() telegraf.Input {
return &RedisSentinel{}
})
}

View file

@ -0,0 +1,373 @@
package redis_sentinel
import (
"bufio"
"bytes"
"fmt"
"os"
"testing"
"time"
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/network"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil"
)
const masterName = "mymaster"
const sentinelServicePort = "26379"
func TestRedisSentinelConnectIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
net, err := network.New(t.Context())
require.NoError(t, err)
defer func() {
require.NoError(t, net.Remove(t.Context()), "terminating network failed")
}()
redis := createRedisContainer(net.Name)
err = redis.Start()
require.NoError(t, err, "failed to start container")
defer redis.Terminate()
firstSentinel := createSentinelContainer(redis.Name, net.Name, wait.ForAll(
wait.ForLog("+monitor master"),
wait.ForListeningPort(nat.Port(sentinelServicePort)),
))
err = firstSentinel.Start()
require.NoError(t, err, "failed to start container")
defer firstSentinel.Terminate()
secondSentinel := createSentinelContainer(redis.Name, net.Name, wait.ForAll(
wait.ForLog("+sentinel sentinel"),
wait.ForListeningPort(nat.Port(sentinelServicePort)),
))
err = secondSentinel.Start()
require.NoError(t, err, "failed to start container")
defer secondSentinel.Terminate()
addr := fmt.Sprintf("tcp://%s:%s", secondSentinel.Address, secondSentinel.Ports[sentinelServicePort])
r := &RedisSentinel{
Servers: []string{addr},
}
err = r.Init()
require.NoError(t, err, "failed to run Init function")
var acc testutil.Accumulator
err = acc.GatherError(r.Gather)
require.NoError(t, err)
require.True(t, acc.HasMeasurement("redis_sentinel_masters"), "redis_sentinel_masters measurement is missing")
require.True(t, acc.HasMeasurement("redis_sentinel_sentinels"), "redis_sentinel_sentinels measurement is missing")
require.True(t, acc.HasMeasurement("redis_sentinel"), "redis_sentinel measurement is missing")
}
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, sentinelErr := convertSentinelMastersOutput(globalTags, sentinelMastersOutput, nil)
require.NoErrorf(t, sentinelErr, "failed converting output: %v", sentinelErr)
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, sentinelErr := convertSentinelReplicaOutput(globalTags, masterName, replicasOutput)
require.NoErrorf(t, sentinelErr, "failed converting output: %v", sentinelErr)
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, sentinelErr := convertSentinelInfoOutput(globalTags, rdr)
require.NoErrorf(t, sentinelErr, "failed converting output: %v", sentinelErr)
actualMetrics := []telegraf.Metric{
testutil.MustMetric(measurementSentinel, sentinelTags, sentinelFields, now),
}
testutil.RequireMetricsEqual(t, expectedMetrics, actualMetrics)
}
func createRedisContainer(networkName string) testutil.Container {
return testutil.Container{
Image: "redis:7.0-alpine",
Name: "telegraf-test-redis-sentinel-redis",
Networks: []string{networkName},
ExposedPorts: []string{"6379"},
WaitingFor: wait.ForAll(
wait.ForLog("Ready to accept connections"),
wait.ForListeningPort(nat.Port("6379")),
),
}
}
func createSentinelContainer(redisAddress, networkName string, waitingFor wait.Strategy) testutil.Container {
return testutil.Container{
Image: "bitnami/redis-sentinel:7.0",
ExposedPorts: []string{sentinelServicePort},
Networks: []string{networkName},
Env: map[string]string{
"REDIS_MASTER_HOST": redisAddress,
},
WaitingFor: waitingFor,
}
}

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,19 @@
# Read metrics from one or many redis-sentinel servers
[[inputs.redis_sentinel]]
## specify servers via a url matching:
## [protocol://][username:password]@address[:port]
## e.g.
## tcp://localhost:26379
## tcp://username: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

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