diff --git a/exporter/exporter.go b/exporter/exporter.go index b2baf152..8e66110c 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -71,6 +71,7 @@ type Options struct { InclConfigMetrics bool InclModulesMetrics bool InclSearchIndexesMetrics bool + InclSentinelPeerInfo bool CheckSearchIndexes string DisableExportingKeyValues bool ExcludeLatencyHistogramMetrics bool @@ -569,6 +570,7 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) { "search_index_number_of_uses_total": {txt: "Number of times the index has been used", lbls: []string{"index_name"}}, "search_index_cleaning": {txt: "Index deletion flag. A value of 1 indicates index deletion is in progress", lbls: []string{"index_name"}}, "sentinel_master_ckquorum_status": {txt: "Master ckquorum status", lbls: []string{"master_name", "message"}}, + "sentinel_peer_info": {txt: "Other Sentinel peers discovered via SENTINEL SENTINELS (one scrape from one Sentinel)", lbls: []string{"master_name", "master_address", "name", "ip", "port", "runid", "flags"}}, "sentinel_master_ok_sentinels": {txt: "The number of okay sentinels monitoring this master", lbls: []string{"master_name", "master_address"}}, "sentinel_master_ok_slaves": {txt: "The number of okay slaves of the master", lbls: []string{"master_name", "master_address"}}, "sentinel_master_sentinels": {txt: "The number of sentinels monitoring this master", lbls: []string{"master_name", "master_address"}}, diff --git a/exporter/info_test.go b/exporter/info_test.go index d5171c3a..7dc50f72 100644 --- a/exporter/info_test.go +++ b/exporter/info_test.go @@ -43,7 +43,6 @@ func TestKeyspaceStringParser(t *testing.T) { }, } - log.SetLevel(log.DebugLevel) for _, tst := range tsts { if kt, kx, ttl, kc, ok := parseDBKeyspaceString(tst.db, tst.stats); true { diff --git a/exporter/sentinels.go b/exporter/sentinels.go index cbeacab4..fea4d4d0 100644 --- a/exporter/sentinels.go +++ b/exporter/sentinels.go @@ -144,6 +144,13 @@ func (e *Exporter) processSentinelSentinels(ch chan<- prometheus.Metric, sentine // If we are here then this master is in ok state masterOkSentinels := 1 + hasMasterLabels := len(labels) >= 2 + masterName := "" + masterAddr := "" + if hasMasterLabels { + masterName = labels[0] + masterAddr = labels[1] + } for _, sentinelDetail := range sentinelDetails { sentinelDetailMap, err := redis.StringMap(sentinelDetail, nil) @@ -152,14 +159,39 @@ func (e *Exporter) processSentinelSentinels(ch chan<- prometheus.Metric, sentine continue } - sentinelFlags, ok := sentinelDetailMap["flags"] - if !ok { + name := "" + if v, ok := sentinelDetailMap["name"]; ok { + name = v + } + ip := "" + if v, ok := sentinelDetailMap["ip"]; ok { + ip = v + } + port := "" + if v, ok := sentinelDetailMap["port"]; ok { + port = v + } + runid := "" + if v, ok := sentinelDetailMap["runid"]; ok { + runid = v + } + flags := "" + flagsFound := false + if v, ok := sentinelDetailMap["flags"]; ok { + flags = v + flagsFound = true + } + if e.options.InclSentinelPeerInfo && hasMasterLabels { + e.registerConstMetricGauge(ch, "sentinel_peer_info", 1, masterName, masterAddr, name, ip, port, runid, flags) + } + + if !flagsFound { continue } - if strings.Contains(sentinelFlags, "o_down") { + if strings.Contains(flags, "o_down") { continue } - if strings.Contains(sentinelFlags, "s_down") { + if strings.Contains(flags, "s_down") { continue } masterOkSentinels = masterOkSentinels + 1 @@ -188,7 +220,9 @@ func (e *Exporter) processSentinelSlaves(ch chan<- prometheus.Metric, slaveDetai } masterOkSlaves = masterOkSlaves + 1 } - e.registerConstMetricGauge(ch, "sentinel_master_ok_slaves", float64(masterOkSlaves), labels...) + if len(labels) >= 2 { + e.registerConstMetricGauge(ch, "sentinel_master_ok_slaves", float64(masterOkSlaves), labels...) + } } /* diff --git a/exporter/sentinels_test.go b/exporter/sentinels_test.go index b1a2973b..3bf6477b 100644 --- a/exporter/sentinels_test.go +++ b/exporter/sentinels_test.go @@ -199,10 +199,11 @@ func TestSentinelExtractSentinelMetricsForSentinel(t *testing.T) { } type sentinelSentinelsData struct { - name string - sentinelDetails []any - labels []string - expectedMetricValue map[string]int + name string + sentinelDetails []any + labels []string + expectedMetricValue map[string]int + expectedPeerInfoCount int } func TestSentinelProcessSentinels(t *testing.T) { @@ -212,7 +213,7 @@ func TestSentinelProcessSentinels(t *testing.T) { addr := os.Getenv("TEST_VALKEY_SENTINEL_URI") e, _ := NewRedisExporter( addr, - Options{Namespace: "test"}, + Options{Namespace: "test", InclSentinelPeerInfo: true}, ) oneOkSentinelExpectedMetricValue := map[string]int{ @@ -222,11 +223,11 @@ func TestSentinelProcessSentinels(t *testing.T) { "sentinel_master_ok_sentinels": 2, } tsts := []sentinelSentinelsData{ - {"1/1 okay sentinel", []any{[]any{[]byte("")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue}, - {"1/3 okay sentinel", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("o_down,s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue}, - {"2/3 okay sentinel(string is not byte slice)", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), "sentinel", []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, - {"2/3 okay sentinel", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, - {"2/3 okay sentinel(missing flags)", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, + {"1/1 okay sentinel", []any{[]any{[]byte("")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue, 0}, + {"1/3 okay sentinel", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("o_down,s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue, 2}, + {"2/3 okay sentinel(string is not byte slice)", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), "sentinel", []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue, 1}, + {"2/3 okay sentinel", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue, 2}, + {"2/3 okay sentinel(missing flags)", []any{[]any{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []any{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue, 2}, } for _, tst := range tsts { t.Run(tst.name, func(t *testing.T) { @@ -237,11 +238,21 @@ func TestSentinelProcessSentinels(t *testing.T) { }() want := map[string]bool{ "sentinel_master_ok_sentinels": false, + "sentinel_peer_info": false, } + peerInfoCount := 0 for m := range chM { + descStr := m.Desc().String() + if strings.Contains(descStr, "sentinel_peer_info") { + peerInfoCount++ + want["sentinel_peer_info"] = true + } for k := range want { - if strings.Contains(m.Desc().String(), k) { + if k == "sentinel_peer_info" { + continue + } + if strings.Contains(descStr, k) { want[k] = true got := &dto.Metric{} m.Write(got) @@ -253,7 +264,15 @@ func TestSentinelProcessSentinels(t *testing.T) { } } } + if tst.expectedPeerInfoCount > 0 { + if peerInfoCount != tst.expectedPeerInfoCount { + t.Errorf("sentinel_peer_info: expected count %d, got %d", tst.expectedPeerInfoCount, peerInfoCount) + } + } for k, found := range want { + if k == "sentinel_peer_info" && tst.expectedPeerInfoCount == 0 { + continue + } if !found { t.Errorf("didn't find metric %s", k) } @@ -269,6 +288,178 @@ type sentinelSlavesData struct { expectedMetricValue map[string]int } +// TestSentinelPeerInfoMetric verifies sentinel_peer_info is emitted with correct labels (no live Sentinel required). +func TestSentinelPeerInfoMetric(t *testing.T) { + e, err := NewRedisExporter("redis://localhost:26379", Options{Namespace: "test", InclSentinelPeerInfo: true}) + if err != nil { + t.Fatalf("NewRedisExporter: %v", err) + } + // One peer with all labels; one peer with missing "flags" (tests safe map access). + sentinelDetails := []any{ + []any{ + []byte("name"), []byte("runid-peer1"), + []byte("ip"), []byte("10.0.0.1"), + []byte("port"), []byte("26379"), + []byte("runid"), []byte("runid-peer1"), + []byte("flags"), []byte("sentinel"), + }, + []any{ + []byte("name"), []byte("runid-peer2"), + []byte("ip"), []byte("10.0.0.2"), + []byte("port"), []byte("26380"), + []byte("runid"), []byte("runid-peer2"), + // no "flags" key - must not panic, label should be empty + }, + } + labels := []string{"mymaster", "127.0.0.1:6379"} + + chM := make(chan prometheus.Metric, 16) + go func() { + e.processSentinelSentinels(chM, sentinelDetails, labels...) + close(chM) + }() + + var peerInfoMetrics []*dto.Metric + for m := range chM { + if strings.Contains(m.Desc().String(), "sentinel_peer_info") { + got := &dto.Metric{} + _ = m.Write(got) + peerInfoMetrics = append(peerInfoMetrics, got) + } + } + + if len(peerInfoMetrics) != 2 { + t.Fatalf("expected 2 sentinel_peer_info metrics, got %d", len(peerInfoMetrics)) + } + + labelMap := func(metric *dto.Metric) map[string]string { + out := make(map[string]string) + for _, lp := range metric.GetLabel() { + out[lp.GetName()] = lp.GetValue() + } + return out + } + + // First peer: all labels set + l0 := labelMap(peerInfoMetrics[0]) + if l0["master_name"] != "mymaster" || l0["master_address"] != "127.0.0.1:6379" { + t.Errorf("first peer: wrong master labels: master_name=%q master_address=%q", l0["master_name"], l0["master_address"]) + } + if l0["name"] != "runid-peer1" || l0["ip"] != "10.0.0.1" || l0["port"] != "26379" || l0["runid"] != "runid-peer1" || l0["flags"] != "sentinel" { + t.Errorf("first peer: wrong peer labels: name=%q ip=%q port=%q runid=%q flags=%q", l0["name"], l0["ip"], l0["port"], l0["runid"], l0["flags"]) + } + + // Second peer: flags missing in input -> must be empty string (no panic) + l1 := labelMap(peerInfoMetrics[1]) + if l1["name"] != "runid-peer2" || l1["ip"] != "10.0.0.2" || l1["port"] != "26380" || l1["runid"] != "runid-peer2" { + t.Errorf("second peer: wrong labels: name=%q ip=%q port=%q runid=%q", l1["name"], l1["ip"], l1["port"], l1["runid"]) + } + if l1["flags"] != "" { + t.Errorf("second peer: expected empty flags when key missing, got %q", l1["flags"]) + } +} + +func TestInclSentinelPeerInfoDisabled(t *testing.T) { + e, err := NewRedisExporter("redis://localhost:26379", Options{Namespace: "test"}) + if err != nil { + t.Fatalf("NewRedisExporter: %v", err) + } + sentinelDetails := []any{ + []any{ + []byte("name"), []byte("peer-a"), + []byte("ip"), []byte("10.0.0.1"), + []byte("port"), []byte("26379"), + []byte("runid"), []byte("rid-a"), + []byte("flags"), []byte("sentinel"), + }, + } + chM := make(chan prometheus.Metric, 8) + go func() { + e.processSentinelSentinels(chM, sentinelDetails, "mymaster", "127.0.0.1:6379") + close(chM) + }() + var peerInfo, okSentinels int + for m := range chM { + ds := m.Desc().String() + if strings.Contains(ds, "sentinel_peer_info") { + peerInfo++ + } + if strings.Contains(ds, "sentinel_master_ok_sentinels") { + okSentinels++ + } + } + if peerInfo != 0 { + t.Errorf("expected no sentinel_peer_info when InclSentinelPeerInfo is false, got %d", peerInfo) + } + if okSentinels != 1 { + t.Errorf("expected sentinel_master_ok_sentinels, got count %d", okSentinels) + } +} + +func TestSentinelProcessSentinelsWithoutLabels(t *testing.T) { + e, err := NewRedisExporter("redis://localhost:26379", Options{Namespace: "test", InclSentinelPeerInfo: true}) + if err != nil { + t.Fatalf("NewRedisExporter: %v", err) + } + + sentinelDetails := []any{ + []any{ + []byte("name"), []byte("peer-a"), + []byte("ip"), []byte("10.0.0.1"), + []byte("port"), []byte("26379"), + []byte("runid"), []byte("rid-a"), + []byte("flags"), []byte("sentinel"), + }, + } + + chM := make(chan prometheus.Metric, 8) + go func() { + e.processSentinelSentinels(chM, sentinelDetails) + close(chM) + }() + + gotAny := false + for range chM { + gotAny = true + } + + if gotAny { + t.Errorf("expected no sentinel metrics when labels are missing") + } +} + +func TestSentinelProcessSlavesWithoutLabels(t *testing.T) { + e, err := NewRedisExporter("redis://localhost:26379", Options{Namespace: "test"}) + if err != nil { + t.Fatalf("NewRedisExporter: %v", err) + } + + slaveDetails := []any{ + []any{ + []byte("name"), []byte("172.17.0.3:6379"), + []byte("ip"), []byte("172.17.0.3"), + []byte("port"), []byte("6379"), + []byte("runid"), []byte("rid-a"), + []byte("flags"), []byte("slave"), + }, + } + + chM := make(chan prometheus.Metric, 8) + go func() { + e.processSentinelSlaves(chM, slaveDetails) + close(chM) + }() + + gotAny := false + for range chM { + gotAny = true + } + + if gotAny { + t.Errorf("expected no slave metrics when labels are missing") + } +} + func TestSentinelProcessSlaves(t *testing.T) { if os.Getenv("TEST_VALKEY_SENTINEL_URI") == "" { t.Skipf("TEST_VALKEY_SENTINEL_URI not set - skipping") diff --git a/main.go b/main.go index 8f2fb120..2d9c6648 100644 --- a/main.go +++ b/main.go @@ -198,6 +198,7 @@ func main() { inclConfigMetrics = flag.Bool("include-config-metrics", getEnvBool("REDIS_EXPORTER_INCL_CONFIG_METRICS", false), "Whether to include all config settings as metrics") inclModulesMetrics = flag.Bool("include-modules-metrics", getEnvBool("REDIS_EXPORTER_INCL_MODULES_METRICS", false), "Whether to collect Redis Modules metrics") inclSearchIndexesMetrics = flag.Bool("include-search-indexes-metrics", getEnvBool("REDIS_EXPORTER_INCL_SEARCH_INDEXES_METRICS", false), "Whether to collect Redis Search indexes metrics") + inclSentinelPeerInfo = flag.Bool("include-sentinel-peer-info", getEnvBool("REDIS_EXPORTER_INCL_SENTINEL_PEER_INFO", false), "Whether to export sentinel_peer_info metrics (high cardinality)") checkSearchIndexes = flag.String("check-search-indexes", getEnv("REDIS_EXPORTER_CHECK_SEARCH_INDEXES", ".*"), "Regex pattern for Redis Search indexes to export metrics from FT.INFO command") disableExportingKeyValues = flag.Bool("disable-exporting-key-values", getEnvBool("REDIS_EXPORTER_DISABLE_EXPORTING_KEY_VALUES", false), "Whether to disable values of keys stored in redis as labels or not when using check-keys/check-single-key") excludeLatencyHistogramMetrics = flag.Bool("exclude-latency-histogram-metrics", getEnvBool("REDIS_EXPORTER_EXCLUDE_LATENCY_HISTOGRAM_METRICS", false), "Do not try to collect latency histogram metrics") @@ -285,6 +286,7 @@ func main() { ClusterDiscoverHostnames: *clusterDiscoverHostnames, InclModulesMetrics: *inclModulesMetrics, InclSearchIndexesMetrics: *inclSearchIndexesMetrics, + InclSentinelPeerInfo: *inclSentinelPeerInfo, CheckSearchIndexes: *checkSearchIndexes, ExportClientList: *exportClientList, ExportClientsInclPort: *exportClientPort,