From 1f06f2f20cbb6be400d0a3b18d078e9096a01293 Mon Sep 17 00:00:00 2001 From: Nicolai Antiferov <75987872+nantiferov@users.noreply.github.com> Date: Sat, 5 Oct 2024 20:22:10 +0300 Subject: [PATCH] Feature: Add redis search module metrics (#953) * Feature: Add redis search module metrics --- Makefile | 3 +- README.md | 1 + docker-compose.yml | 5 +++ exporter/exporter.go | 21 ++++++++++++ exporter/http_test.go | 1 + exporter/modules.go | 52 ++++++++++++++++++++++++++++ exporter/modules_test.go | 74 ++++++++++++++++++++++++++++++++++++++++ main.go | 2 ++ 8 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 exporter/modules.go create mode 100644 exporter/modules_test.go diff --git a/Makefile b/Makefile index 9a7cb778..ab092cc8 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,8 @@ test: TEST_REDIS_CLUSTER_PASSWORD_URI="redis://localhost:17006" \ TEST_TILE38_URI="redis://localhost:19851" \ TEST_REDIS_SENTINEL_URI="redis://localhost:26379" \ - go test -v -covermode=atomic -cover -race -coverprofile=coverage.txt -p 1 ./... + TEST_REDIS_MODULES_URI="redis://localhost:36379" \ + go test -v -covermode=atomic -cover -race -coverprofile=coverage.txt -p 1 ./... .PHONY: lint lint: diff --git a/README.md b/README.md index dd1948e1..2cc1cdd2 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Prometheus uses file watches and all changes to the json file are applied immedi | redis-only-metrics | REDIS_EXPORTER_REDIS_ONLY_METRICS | Whether to also export go runtime metrics, defaults to false. | | include-config-metrics | REDIS_EXPORTER_INCL_CONFIG_METRICS | Whether to include all config settings as metrics, defaults to false. | | include-system-metrics | REDIS_EXPORTER_INCL_SYSTEM_METRICS | Whether to include system metrics like `total_system_memory_bytes`, defaults to false. | +| include-modules-metrics | REDIS_EXPORTER_INCL_MODULES_METRICS | Whether to collect Redis Modules metrics, defaults to false. | | exclude-latency-histogram-metrics | REDIS_EXPORTER_EXCLUDE_LATENCY_HISTOGRAM_METRICS | Do not try to collect latency histogram metrics (to avoid `WARNING, LOGGED ONCE ONLY: cmd LATENCY HISTOGRAM` error on Redis < v7). | | redact-config-metrics | REDIS_EXPORTER_REDACT_CONFIG_METRICS | Whether to redact config settings that include potentially sensitive information like passwords. | | ping-on-connect | REDIS_EXPORTER_PING_ON_CONNECT | Whether to ping the redis instance after connecting and record the duration as a metric, defaults to false. | diff --git a/docker-compose.yml b/docker-compose.yml index 9e4202c9..25da5938 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,3 +83,8 @@ services: image: tile38/tile38:latest ports: - "19851:9851" + + redis-stack: + image: redis/redis-stack-server:7.4.0-v0 + ports: + - "36379:6379" diff --git a/exporter/exporter.go b/exporter/exporter.go index 756087d6..6abafbc6 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -65,6 +65,7 @@ type Options struct { ClientKeyFile string CaCertFile string InclConfigMetrics bool + InclModulesMetrics bool DisableExportingKeyValues bool ExcludeLatencyHistogramMetrics bool RedactConfigMetrics bool @@ -267,6 +268,21 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) { "server_threads": "server_threads_total", "long_lock_waits": "long_lock_waits_total", "current_client_thread": "current_client_thread", + + // Redis Modules metrics + // RediSearch module + "search_number_of_indexes": "search_number_of_indexes", + "search_used_memory_indexes": "search_used_memory_indexes_bytes", + "search_total_indexing_time": "search_total_indexing_time_ms", + "search_global_idle": "search_global_idle", + "search_global_total": "search_global_total", + "search_bytes_collected": "search_collected_bytes", + "search_total_cycles": "search_total_cycles", + "search_total_ms_run": "search_total_run_ms", + "search_dialect_1": "search_dialect_1", + "search_dialect_2": "search_dialect_2", + "search_dialect_3": "search_dialect_3", + "search_dialect_4": "search_dialect_4", }, metricMapCounters: map[string]string{ @@ -421,6 +437,7 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) { "stream_radix_tree_keys": {txt: `Radix tree keys count"`, lbls: []string{"db", "stream"}}, "stream_radix_tree_nodes": {txt: `Radix tree nodes count`, lbls: []string{"db", "stream"}}, "up": {txt: "Information about the Redis instance"}, + "module_info": {txt: "Information about loaded Redis module", lbls: []string{"name", "ver", "api", "filters", "usedby", "using"}}, } { e.metricDescriptions[k] = newMetricDescr(opts.Namespace, k, desc.txt, desc.lbls) } @@ -698,6 +715,10 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { e.extractTile38Metrics(ch, c) } + if e.options.InclModulesMetrics { + e.extractModulesMetrics(ch, c) + } + if len(e.options.LuaScript) > 0 { for filename, script := range e.options.LuaScript { if err := e.extractLuaScriptMetrics(ch, c, filename, script); err != nil { diff --git a/exporter/http_test.go b/exporter/http_test.go index db672a93..203ab143 100644 --- a/exporter/http_test.go +++ b/exporter/http_test.go @@ -210,6 +210,7 @@ func TestSimultaneousMetricsHttpRequests(t *testing.T) { os.Getenv("TEST_REDIS5_URI"), os.Getenv("TEST_REDIS6_URI"), + os.Getenv("TEST_REDIS_MODULES_URI"), // tile38 & Cluster need to be last in this list so we can identify them when selected, down in line 229 os.Getenv("TEST_REDIS_CLUSTER_MASTER_URI"), diff --git a/exporter/modules.go b/exporter/modules.go new file mode 100644 index 00000000..2fe82a70 --- /dev/null +++ b/exporter/modules.go @@ -0,0 +1,52 @@ +package exporter + +import ( + "strings" + + "github.com/gomodule/redigo/redis" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +func (e *Exporter) extractModulesMetrics(ch chan<- prometheus.Metric, c redis.Conn) { + info, err := redis.String(doRedisCmd(c, "INFO", "MODULES")) + if err != nil { + log.Errorf("extractSearchMetrics() err: %s", err) + return + } + + lines := strings.Split(info, "\r\n") + for _, line := range lines { + log.Debugf("info: %s", line) + + split := strings.Split(line, ":") + if len(split) != 2 { + continue + } + + if split[0] == "module" { + // module format: 'module:name=,ver=21005,api=1,filters=0,usedby=[],using=[],options=[]' + module := strings.Split(split[1], ",") + if len(module) != 7 { + continue + } + e.registerConstMetricGauge(ch, "module_info", 1, + strings.Split(module[0], "=")[1], + strings.Split(module[1], "=")[1], + strings.Split(module[2], "=")[1], + strings.Split(module[3], "=")[1], + strings.Split(module[4], "=")[1], + strings.Split(module[5], "=")[1], + ) + continue + } + + fieldKey := split[0] + fieldValue := split[1] + + if !e.includeMetric(fieldKey) { + continue + } + e.parseAndRegisterConstMetric(ch, fieldKey, fieldValue) + } +} diff --git a/exporter/modules_test.go b/exporter/modules_test.go new file mode 100644 index 00000000..6e6631d8 --- /dev/null +++ b/exporter/modules_test.go @@ -0,0 +1,74 @@ +package exporter + +import ( + "os" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestModules(t *testing.T) { + if os.Getenv("TEST_REDIS_MODULES_URI") == "" { + t.Skipf("TEST_REDIS_MODULES_URI not set - skipping") + } + + tsts := []struct { + addr string + inclModulesMetrics bool + wantModulesMetrics bool + }{ + {addr: os.Getenv("TEST_REDIS_MODULES_URI"), inclModulesMetrics: true, wantModulesMetrics: true}, + {addr: os.Getenv("TEST_REDIS_MODULES_URI"), inclModulesMetrics: false, wantModulesMetrics: false}, + {addr: os.Getenv("TEST_REDIS_URI"), inclModulesMetrics: true, wantModulesMetrics: false}, + {addr: os.Getenv("TEST_REDIS_URI"), inclModulesMetrics: false, wantModulesMetrics: false}, + } + + for _, tst := range tsts { + e, _ := NewRedisExporter(tst.addr, Options{Namespace: "test", InclModulesMetrics: tst.inclModulesMetrics}) + + chM := make(chan prometheus.Metric) + go func() { + e.Collect(chM) + close(chM) + }() + + wantedMetrics := map[string]bool{ + "module_info": false, + "search_number_of_indexes": false, + "search_used_memory_indexes_bytes": false, + "search_total_indexing_time_ms": false, + "search_global_idle": false, + "search_global_total": false, + "search_collected_bytes": false, + "search_total_cycles": false, + "search_total_run_ms": false, + "search_dialect_1": false, + "search_dialect_2": false, + "search_dialect_3": false, + "search_dialect_4": false, + } + + for m := range chM { + for want := range wantedMetrics { + if strings.Contains(m.Desc().String(), want) { + wantedMetrics[want] = true + } + } + } + + if tst.wantModulesMetrics { + for want, found := range wantedMetrics { + if !found { + t.Errorf("%s was *not* found in Redis Modules metrics but expected", want) + } + } + } else if !tst.wantModulesMetrics { + for want, found := range wantedMetrics { + if found { + t.Errorf("%s was *found* in Redis Modules metrics but *not* expected", want) + } + } + } + } +} diff --git a/main.go b/main.go index 90215d56..fdb1bc07 100644 --- a/main.go +++ b/main.go @@ -94,6 +94,7 @@ func main() { redisMetricsOnly = flag.Bool("redis-only-metrics", getEnvBool("REDIS_EXPORTER_REDIS_ONLY_METRICS", false), "Whether to also export go runtime metrics") pingOnConnect = flag.Bool("ping-on-connect", getEnvBool("REDIS_EXPORTER_PING_ON_CONNECT", false), "Whether to ping the redis instance after connecting") 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") 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") redactConfigMetrics = flag.Bool("redact-config-metrics", getEnvBool("REDIS_EXPORTER_REDACT_CONFIG_METRICS", true), "Whether to redact config settings that include potentially sensitive information like passwords") @@ -182,6 +183,7 @@ func main() { SetClientName: *setClientName, IsTile38: *isTile38, IsCluster: *isCluster, + InclModulesMetrics: *inclModulesMetrics, ExportClientList: *exportClientList, ExportClientsInclPort: *exportClientPort, SkipTLSVerification: *skipTLSVerification,