Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add -redis.file option back to support monitoring of multiple Redis dbs #332

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ and adjust the host name accordingly.

### Prometheus Configuration to Scrape Multiple Redis Hosts

Run the exporter with the command line flag `--redis.addr=` so it won't try to access
Run the exporter with the command line flag `--redis.addr=` so it won't try to access
the local instance every time the `/metrics` endpoint is scraped.

```yaml
Expand All @@ -63,7 +63,7 @@ scrape_configs:
target_label: instance
- target_label: __address__
replacement: <<REDIS-EXPORTER-HOSTNAME>>:9121

## config for scraping the exporter itself
- job_name: 'redis_exporter'
static_configs:
Expand All @@ -73,7 +73,7 @@ scrape_configs:

The Redis instances are listed under `targets`, the Redis exporter hostname is configured via the last relabel_config rule.\
If authentication is needed for the Redis instances then you can set the password via the `--redis.password` command line option of
the exporter (this means you can currently only use one password across the instances you try to scrape this way. Use several
the exporter (this means you can currently only use one password across the instances you try to scrape this way. Use several
exporters if this is a problem). \
You can also use a json file to supply multiple targets by using `file_sd_configs` like so:

Expand Down Expand Up @@ -121,6 +121,7 @@ Name | Environment Variable Name | Description
-----------------------|--------------------------------------|-----------------
redis.addr | REDIS_ADDR | Address of the Redis instance, defaults to `redis://localhost:6379`.
redis.password | REDIS_PASSWORD | Password of the Redis instance, defaults to `""` (no password).
redis.file | REDIS_FILE | Path to file containing one or more redis nodes, separated by newline. Format: `<redis URI>,<optional password>,<optional alias>` NOTE: mutually exclusive with redis.addr
check-keys | REDIS_EXPORTER_CHECK_KEYS | Comma separated list of key patterns to export value and length/size, eg: `db3=user_count` will export key `user_count` from db `3`. db defaults to `0` if omitted. The key patterns specified with this flag will be found using [SCAN](https://redis.io/commands/scan). Use this option if you need glob pattern matching; `check-single-keys` is faster for non-pattern keys. Warning: using `--check-keys` to match a very large number of keys can slow down the exporter to the point where it doesn't finish scraping the redis instance.
check-single-keys | REDIS_EXPORTER_CHECK_SINGLE_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. The keys specified with this flag will be looked up directly without any glob pattern matching. Use this option if you don't need glob pattern matching; it is faster than `check-keys`.
script | REDIS_EXPORTER_SCRIPT | Path to Redis Lua script for gathering extra metrics.
Expand Down
128 changes: 96 additions & 32 deletions exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package main

import (
"crypto/tls"
"encoding/csv"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
Expand All @@ -29,11 +31,19 @@ type keyInfo struct {
keyType string
}

type serverInfo struct {
addr string
password string
alias string
addConstLabels bool
}

// Exporter implements the prometheus.Exporter interface, and exports Redis metrics.
type Exporter struct {
sync.Mutex
redisAddr string
namespace string
server serverInfo
constLabels prometheus.Labels
namespace string

totalScrapes prometheus.Counter
scrapeDuration prometheus.Summary
Expand Down Expand Up @@ -143,33 +153,53 @@ func parseKeyArg(keysArgString string) (keys []dbKeyPair, err error) {
return keys, err
}

func newMetricDescr(namespace string, metricName string, docString string, labels []string) *prometheus.Desc {
return prometheus.NewDesc(prometheus.BuildFQName(namespace, "", metricName), docString, labels, nil)
func newMetricDescr(namespace string, metricName string, docString string, labels []string, constLabels prometheus.Labels) *prometheus.Desc {
return prometheus.NewDesc(prometheus.BuildFQName(namespace, "", metricName), docString, labels, constLabels)
}

// NewRedisExporter returns a new exporter of Redis metrics.
func NewRedisExporter(redisURI string, opts ExporterOptions) (*Exporter, error) {
func NewRedisExporter(serverArg interface{}, opts ExporterOptions) (*Exporter, error) {
var server serverInfo
switch v := serverArg.(type) {
case serverInfo:
server = v
case string:
server.addr = v
}

constLabels := prometheus.Labels{}
if server.addConstLabels {
if server.alias == "" {
server.alias = server.addr
}
constLabels = prometheus.Labels{"addr": server.addr, "alias": server.alias}
}

e := &Exporter{
redisAddr: redisURI,
options: opts,
namespace: opts.Namespace,
server: server,
constLabels: constLabels,
options: opts,
namespace: opts.Namespace,

totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: opts.Namespace,
Name: "exporter_scrapes_total",
Help: "Current total redis scrapes.",
Namespace: opts.Namespace,
Name: "exporter_scrapes_total",
Help: "Current total redis scrapes.",
ConstLabels: constLabels,
}),

scrapeDuration: prometheus.NewSummary(prometheus.SummaryOpts{
Namespace: opts.Namespace,
Name: "exporter_scrape_duration_seconds",
Help: "Duration of scrape by the exporter",
Namespace: opts.Namespace,
Name: "exporter_scrape_duration_seconds",
Help: "Duration of scrape by the exporter",
ConstLabels: constLabels,
}),

targetScrapeRequestErrors: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: opts.Namespace,
Name: "target_scrape_request_errors_total",
Help: "Errors in requests to the exporter",
Namespace: opts.Namespace,
Name: "target_scrape_request_errors_total",
Help: "Errors in requests to the exporter",
ConstLabels: constLabels,
}),

metricMapGauges: map[string]string{
Expand Down Expand Up @@ -322,7 +352,7 @@ func NewRedisExporter(redisURI string, opts ExporterOptions) (*Exporter, error)
"up": {txt: "Information about the Redis instance"},
"connected_clients_details": {txt: "Details about connected clients", lbls: []string{"host", "port", "name", "age", "idle", "flags", "db", "cmd"}},
} {
e.metricDescriptions[k] = newMetricDescr(opts.Namespace, k, desc.txt, desc.lbls)
e.metricDescriptions[k] = newMetricDescr(opts.Namespace, k, desc.txt, desc.lbls, constLabels)
}

if e.options.MetricsPath == "" {
Expand All @@ -339,9 +369,10 @@ func NewRedisExporter(redisURI string, opts ExporterOptions) (*Exporter, error)

if !e.options.RedisMetricsOnly {
buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: opts.Namespace,
Name: "exporter_build_info",
Help: "redis exporter build_info",
Namespace: opts.Namespace,
Name: "exporter_build_info",
Help: "redis exporter build_info",
ConstLabels: constLabels,
}, []string{"version", "commit_sha", "build_date", "golang_version"})
buildInfo.WithLabelValues(BuildVersion, BuildCommitSha, BuildDate, runtime.Version()).Set(1)
e.options.Registry.MustRegister(buildInfo)
Expand Down Expand Up @@ -377,11 +408,11 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
}

for _, v := range e.metricMapGauges {
ch <- newMetricDescr(e.options.Namespace, v, v+" metric", nil)
ch <- newMetricDescr(e.options.Namespace, v, v+" metric", nil, e.constLabels)
}

for _, v := range e.metricMapCounters {
ch <- newMetricDescr(e.options.Namespace, v, v+" metric", nil)
ch <- newMetricDescr(e.options.Namespace, v, v+" metric", nil, e.constLabels)
}

ch <- e.totalScrapes.Desc()
Expand All @@ -395,7 +426,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
defer e.Unlock()
e.totalScrapes.Inc()

if e.redisAddr != "" {
if e.server.addr != "" {
start := time.Now().UnixNano()
var up float64 = 1
if err := e.scrapeRedisHost(ch); err != nil {
Expand Down Expand Up @@ -596,7 +627,7 @@ func (e *Exporter) registerConstMetricGauge(ch chan<- prometheus.Metric, metric
func (e *Exporter) registerConstMetric(ch chan<- prometheus.Metric, metric string, val float64, valType prometheus.ValueType, labelValues ...string) {
descr := e.metricDescriptions[metric]
if descr == nil {
descr = newMetricDescr(e.options.Namespace, metric, metric+" metric", nil)
descr = newMetricDescr(e.options.Namespace, metric, metric+" metric", nil, e.constLabels)
}

if m, err := prometheus.NewConstMetric(descr, valType, val, labelValues...); err == nil {
Expand Down Expand Up @@ -1098,24 +1129,26 @@ func (e *Exporter) connectToRedis() (redis.Conn, error) {
}),
}

if e.options.Password != "" {
if e.server.password != "" {
options = append(options, redis.DialPassword(e.server.password))
} else if e.options.Password != "" {
options = append(options, redis.DialPassword(e.options.Password))
}

uri := e.redisAddr
uri := e.server.addr
if !strings.Contains(uri, "://") {
uri = "redis://" + uri
}
log.Debugf("Trying DialURL(): %s", uri)
c, err := redis.DialURL(uri, options...)
if err != nil {
log.Debugf("DialURL() failed, err: %s", err)
if frags := strings.Split(e.redisAddr, "://"); len(frags) == 2 {
if frags := strings.Split(e.server.addr, "://"); len(frags) == 2 {
log.Debugf("Trying: Dial(): %s %s", frags[0], frags[1])
c, err = redis.Dial(frags[0], frags[1], options...)
} else {
log.Debugf("Trying: Dial(): tcp %s", e.redisAddr)
c, err = redis.Dial("tcp", e.redisAddr, options...)
log.Debugf("Trying: Dial(): tcp %s", e.server.addr)
c, err = redis.Dial("tcp", e.server.addr, options...)
}
}
return c, err
Expand All @@ -1125,12 +1158,12 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error {
c, err := e.connectToRedis()
if err != nil {
log.Errorf("Couldn't connect to redis instance")
log.Debugf("connectToRedis( %s ) err: %s", e.redisAddr, err)
log.Debugf("connectToRedis( %s ) err: %s", e.server.addr, err)
return err
}
defer c.Close()

log.Debugf("connected to: %s", e.redisAddr)
log.Debugf("connected to: %s", e.server.addr)

if _, err := doRedisCmd(c, "CLIENT", "SETNAME", "redis_exporter"); err != nil {
log.Errorf("Couldn't set client name, err: %s", err)
Expand Down Expand Up @@ -1198,3 +1231,34 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error {
log.Debugf("scrapeRedisHost() done")
return nil
}

// loadRedisFile opens the specified file and loads the configuration for which redis
// hosts to monitor. Returns the list of hosts addrs, passwords, and their aliases.
func loadRedisFile(fileName string) ([]serverInfo, error) {
var servers []serverInfo
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
r := csv.NewReader(file)
r.FieldsPerRecord = -1
records, err := r.ReadAll()
if err != nil {
return nil, err
}
file.Close()
// For each line, test if it contains an optional password and alias and provide them,
// else give them empty strings
for _, record := range records {
length := len(record)
switch length {
case 3:
servers = append(servers, serverInfo{addr: record[0], password: record[1], alias: record[2]})
case 2:
servers = append(servers, serverInfo{addr: record[0], password: record[1]})
case 1:
servers = append(servers, serverInfo{addr: record[0]})
}
}
return servers, nil
}
61 changes: 38 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func main() {
var (
redisAddr = flag.String("redis.addr", getEnv("REDIS_ADDR", "redis://localhost:6379"), "Address of the Redis instance to scrape")
redisPwd = flag.String("redis.password", getEnv("REDIS_PASSWORD", ""), "Password of the Redis instance to scrape")
redisFile = flag.String("redis.file", getEnv("REDIS_FILE", ""), "Path to file containing one or more redis nodes, separated by newline. NOTE: mutually exclusive with redis.addr")
namespace = flag.String("namespace", getEnv("REDIS_EXPORTER_NAMESPACE", "redis"), "Namespace for metrics")
checkKeys = flag.String("check-keys", getEnv("REDIS_EXPORTER_CHECK_KEYS", ""), "Comma separated list of key-patterns to export value and length/size, searched for with SCAN")
checkSingleKeys = flag.String("check-single-keys", getEnv("REDIS_EXPORTER_CHECK_SINGLE_KEYS", ""), "Comma separated list of single keys to export value and length/size")
Expand Down Expand Up @@ -112,31 +113,45 @@ func main() {
registry = prometheus.DefaultRegisterer.(*prometheus.Registry)
}

exp, err := NewRedisExporter(
*redisAddr,
ExporterOptions{
Password: *redisPwd,
Namespace: *namespace,
ConfigCommandName: *configCommand,
CheckKeys: *checkKeys,
CheckSingleKeys: *checkSingleKeys,
LuaScript: ls,
InclSystemMetrics: *inclSystemMetrics,
IsTile38: *isTile38,
ExportClientList: *exportClientList,
SkipTLSVerification: *skipTLSVerification,
ClientCertificates: tlsClientCertificates,
ConnectionTimeouts: to,
MetricsPath: *metricPath,
RedisMetricsOnly: *redisMetricsOnly,
Registry: registry,
},
)
if err != nil {
log.Fatal(err)
servers := []serverInfo{serverInfo{addr: *redisAddr, password: *redisPwd, alias: *redisAddr}}
if *redisFile != "" {
if servers, err = loadRedisFile(*redisFile); err != nil {
log.Fatal(err)
}
}

var exp *Exporter
for _, server := range servers {
if len(servers) > 1 {
server.addConstLabels = true
}
exp, err = NewRedisExporter(
server,
ExporterOptions{
Password: *redisPwd,
Namespace: *namespace,
ConfigCommandName: *configCommand,
CheckKeys: *checkKeys,
CheckSingleKeys: *checkSingleKeys,
LuaScript: ls,
InclSystemMetrics: *inclSystemMetrics,
IsTile38: *isTile38,
ExportClientList: *exportClientList,
SkipTLSVerification: *skipTLSVerification,
ClientCertificates: tlsClientCertificates,
ConnectionTimeouts: to,
MetricsPath: *metricPath,
RedisMetricsOnly: *redisMetricsOnly,
Registry: registry,
},
)
if err != nil {
log.Fatal(err)
} else {
log.Infof("Configured with redis addr: %s [%s]", server.addr, server.alias)
}
}

log.Infof("Providing metrics at %s%s", *listenAddress, *metricPath)
log.Debugf("Configured redis addr: %#v", *redisAddr)
log.Fatal(http.ListenAndServe(*listenAddress, exp))
}