From 6931a028e5e0265d0390c479f1c7b17e1179de37 Mon Sep 17 00:00:00 2001 From: Konrad Wojas Date: Wed, 6 Jun 2018 17:05:24 +0800 Subject: [PATCH] Optional Redis script for metric collection Allow an optional Redis Lua script to collect extra metrics. This can be enabled with a new `-script` flag. An example is provided in contrib. --- README.md | 2 ++ contrib/sample_collect_script.lua | 21 +++++++++++++++++++++ exporter/redis.go | 27 +++++++++++++++++++++++++++ exporter/redis_test.go | 30 ++++++++++++++++++++++++++++++ main.go | 10 ++++++++++ 5 files changed, 90 insertions(+) create mode 100644 contrib/sample_collect_script.lua diff --git a/README.md b/README.md index 394171b5..1bb3f41c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Name | Description debug | Verbose debug output log-format | Log format, valid options are `txt` (default) and `json`. check-keys | Comma separated list of keys to export value and length/size, eg: `db3=user_count` will export key `user_count` from db `3`. db defaults to `0` if omitted. +script | Path to Redis Lua script for gathering extra metrics. redis.addr | Address of one or more redis nodes, comma separated, defaults to `redis://localhost:6379`. redis.password | Password to use when authenticating to Redis redis.alias | Alias for redis node addr, comma separated. @@ -121,6 +122,7 @@ see http://redis.io/commands/info for details.
In addition, for every database there are metrics for total keys, expiring keys and the average TTL for keys in the database.
You can also export values of keys if they're in numeric format by using the `-check-keys` flag. The exporter will also export the size (or, depending on the data type, the length) of the key. This can be used to export the number of elements in (sorted) sets, hashes, lists, etc.
+If you require custom metric collection, you can provide a [Redis Lua script](https://redis.io/commands/eval) using the `-script` flag. An example can be found [in the contrib folder](./contrib/sample_collect_script.lua). ### What does it look like? Example [Grafana](http://grafana.org/) screenshots:
diff --git a/contrib/sample_collect_script.lua b/contrib/sample_collect_script.lua new file mode 100644 index 00000000..df9b20d7 --- /dev/null +++ b/contrib/sample_collect_script.lua @@ -0,0 +1,21 @@ +-- Example collect script for -script option +-- This returns a Lua table with alternating keys and values. +-- Both keys and values must be strings, similar to a HGETALL result. +-- More info about Redis Lua scripting: https://redis.io/commands/eval + +local result = {} + +-- Add all keys and values from some hash in db 5 +redis.call("SELECT", 5) +local r = redis.call("HGETALL", "some-hash-with-stats") +if r ~= nil then + for _,v in ipairs(r) do + table.insert(result, v) -- alternating keys and values + end +end + +-- Set foo to 42 +table.insert(result, "foo") +table.insert(result, "42") -- note the string, use tostring() if needed + +return result diff --git a/exporter/redis.go b/exporter/redis.go index 180c880a..190f849f 100644 --- a/exporter/redis.go +++ b/exporter/redis.go @@ -33,6 +33,8 @@ type Exporter struct { keys []dbKeyPair keyValues *prometheus.GaugeVec keySizes *prometheus.GaugeVec + script []byte + scriptValues *prometheus.GaugeVec duration prometheus.Gauge scrapeErrors prometheus.Gauge totalScrapes prometheus.Counter @@ -199,6 +201,11 @@ func NewRedisExporter(host RedisHost, namespace, checkKeys string) (*Exporter, e Name: "key_size", Help: "The length or size of \"key\"", }, []string{"addr", "alias", "db", "key"}), + scriptValues: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "script_value", + Help: "Values returned by the collect script", + }, []string{"addr", "alias", "key"}), duration: prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: namespace, Name: "exporter_last_scrape_duration_seconds", @@ -257,6 +264,11 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- e.scrapeErrors.Desc() } +// SetScript sets the Lua Redis script to be used. +func (e *Exporter) SetScript(script []byte) { + e.script = script +} + // Collect fetches new metrics from the RedisHost and updates the appropriate metrics. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { scrapes := make(chan scrapeResult) @@ -273,6 +285,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.keySizes.Collect(ch) e.keyValues.Collect(ch) + e.scriptValues.Collect(ch) ch <- e.duration ch <- e.totalScrapes @@ -705,6 +718,20 @@ func (e *Exporter) scrapeRedisHost(scrapes chan<- scrapeResult, addr string, idx } } + if e.script != nil && len(e.script) > 0 { + log.Debug("e.script") + kv, err := redis.StringMap(doRedisCmd(c, "EVAL", e.script, 0, 0)) + if err != nil { + log.Errorf("Collect script error: %v", err) + } else if kv != nil { + for key, stringVal := range kv { + if val, err := strconv.ParseFloat(stringVal, 64); err == nil { + e.scriptValues.WithLabelValues(addr, e.redis.Aliases[idx], key).Set(val) + } + } + } + } + log.Debugf("scrapeRedisHost() done") return nil } diff --git a/exporter/redis_test.go b/exporter/redis_test.go index 9d2fc7c8..c228973f 100644 --- a/exporter/redis_test.go +++ b/exporter/redis_test.go @@ -503,6 +503,36 @@ func TestKeyValuesAndSizesWildcard(t *testing.T) { } } +func TestScript(t *testing.T) { + + e, _ := NewRedisExporter(defaultRedisHost, "test", "") + e.SetScript([]byte(`return {"a", "11", "b", "12", "c", "13"}`)) + nKeys := 3 + + setupDBKeys(t, defaultRedisHost.Addrs[0]) + defer deleteKeysFromDB(t, defaultRedisHost.Addrs[0]) + + chM := make(chan prometheus.Metric) + go func() { + e.Collect(chM) + close(chM) + }() + + for m := range chM { + switch m.(type) { + case prometheus.Gauge: + if strings.Contains(m.Desc().String(), "test_script_value") { + nKeys-- + } + default: + log.Printf("default: m: %#v", m) + } + } + if nKeys != 0 { + t.Error("didn't find expected script keys") + } +} + func TestKeyValueInvalidDB(t *testing.T) { e, _ := NewRedisExporter(defaultRedisHost, "test", "999="+url.QueryEscape(keys[0])) diff --git a/main.go b/main.go index e490c5f2..7df96fab 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "io/ioutil" "net/http" "os" "runtime" @@ -19,6 +20,7 @@ var ( redisAlias = flag.String("redis.alias", getEnv("REDIS_ALIAS", ""), "Redis instance alias for one or more redis nodes, separated by separator") namespace = flag.String("namespace", "redis", "Namespace for metrics") checkKeys = flag.String("check-keys", "", "Comma separated list of keys to export value and length/size") + scriptPath = flag.String("script", "", "Path to Lua Redis script for collecting extra metrics") separator = flag.String("separator", ",", "separator used to split redis.addr, redis.password and redis.alias into several elements.") listenAddress = flag.String("web.listen-address", ":9121", "Address to listen on for web interface and telemetry.") metricPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") @@ -92,6 +94,14 @@ func main() { log.Fatal(err) } + if *scriptPath != "" { + script, err := ioutil.ReadFile(*scriptPath) + if err != nil { + log.Fatalf("Error loading script file: %v", err) + } + exp.SetScript(script) + } + buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "redis_exporter_build_info", Help: "redis exporter build_info",