Skip to content

Commit

Permalink
Optional Redis script for metric collection
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wojas committed Jun 6, 2018
1 parent 5a57301 commit 6931a02
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down Expand Up @@ -121,6 +122,7 @@ see http://redis.io/commands/info for details.<br>
In addition, for every database there are metrics for total keys, expiring keys and the average TTL for keys in the database.<br>
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. <br>

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:<br>
Expand Down
21 changes: 21 additions & 0 deletions 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
27 changes: 27 additions & 0 deletions exporter/redis.go
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
30 changes: 30 additions & 0 deletions exporter/redis_test.go
Expand Up @@ -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]))
Expand Down
10 changes: 10 additions & 0 deletions main.go
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"io/ioutil"
"net/http"
"os"
"runtime"
Expand All @@ -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.")
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 6931a02

Please sign in to comment.