Skip to content

Commit

Permalink
Merge pull request #29 from oliver006/oh_add_commandstats
Browse files Browse the repository at this point in the history
Add per-command stats
  • Loading branch information
oliver006 authored Oct 11, 2016
2 parents fe11e73 + bc61bcf commit 1a3254a
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 47 deletions.
110 changes: 87 additions & 23 deletions exporter/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Exporter struct {
scrapeErrors prometheus.Gauge
totalScrapes prometheus.Counter
metrics map[string]*prometheus.GaugeVec
metricsMtx sync.RWMutex
sync.RWMutex
}

Expand Down Expand Up @@ -121,6 +122,16 @@ func (e *Exporter) initGauges() {
Name: "db_avg_ttl_seconds",
Help: "Avg TTL in seconds",
}, []string{"addr", "db"})
e.metrics["command_calls_total"] = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: e.namespace,
Name: "command_calls_total",
Help: "Total number of calls per command",
}, []string{"addr", "cmd"})
e.metrics["command_calls_usec_total"] = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: e.namespace,
Name: "command_calls_usec_total",
Help: "Total amount of time in usecs spent per command",
}, []string{"addr", "cmd"})
}

// NewRedisExporter returns a new exporter of Redis metrics.
Expand Down Expand Up @@ -221,17 +232,29 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.collectMetrics(ch)
}

func includeMetric(name string) bool {
func includeMetric(s string) bool {

if strings.HasPrefix(name, "db") {
if strings.HasPrefix(s, "db") || strings.HasPrefix(s, "cmdstat_") {
return true
}

_, ok := inclMap[name]
_, ok := inclMap[s]

return ok
}

func extractVal(s string) (val float64, err error) {
split := strings.Split(s, "=")
if len(split) != 2 {
return 0, fmt.Errorf("nope")
}
val, err = strconv.ParseFloat(split[1], 64)
if err != nil {
return 0, fmt.Errorf("nope")
}
return
}

/*
valid example: db0:keys=1,expires=0,avg_ttl=0
*/
Expand All @@ -246,32 +269,20 @@ func parseDBKeyspaceString(db string, stats string) (keysTotal float64, keysExpi
return
}

extract := func(s string) (val float64, err error) {
split := strings.Split(s, "=")
if len(split) != 2 {
return 0, fmt.Errorf("nope")
}
val, err = strconv.ParseFloat(split[1], 64)
if err != nil {
return 0, fmt.Errorf("nope")
}
return
}

var err error
ok = true
if keysTotal, err = extract(split[0]); err != nil {
if keysTotal, err = extractVal(split[0]); err != nil {
ok = false
return
}
if keysExpiringTotal, err = extract(split[1]); err != nil {
if keysExpiringTotal, err = extractVal(split[1]); err != nil {
ok = false
return
}

avgTTL = -1
if len(split) > 2 {
if avgTTL, err = extract(split[2]); err != nil {
if avgTTL, err = extractVal(split[2]); err != nil {
ok = false
return
}
Expand All @@ -280,17 +291,64 @@ func parseDBKeyspaceString(db string, stats string) (keysTotal float64, keysExpi
return
}

func extractInfoMetrics(info, addr string, scrapes chan<- scrapeResult) error {
func (e *Exporter) extractInfoMetrics(info, addr string, scrapes chan<- scrapeResult) error {
cmdstats := false
lines := strings.Split(info, "\r\n")
for _, line := range lines {

if (len(line) < 2) || line[0] == '#' || (!strings.Contains(line, ":")) {
if len(line) > 0 && line[0] == '#' {
if strings.Contains(line, "Commandstats") {
cmdstats = true
}
continue
}

if (len(line) < 2) || (!strings.Contains(line, ":")) {
cmdstats = false
continue
}

split := strings.Split(line, ":")
if len(split) != 2 || !includeMetric(split[0]) {
continue
}

if cmdstats {
/*
cmdstat_get:calls=21,usec=175,usec_per_call=8.33
cmdstat_set:calls=61,usec=3139,usec_per_call=51.46
cmdstat_setex:calls=75,usec=1260,usec_per_call=16.80
*/

frags := strings.Split(split[0], "_")
if len(frags) != 2 {
continue
}

cmd := frags[1]

frags = strings.Split(split[1], ",")
if len(frags) != 3 {
continue
}

var calls float64
var usecTotal float64
var err error
if calls, err = extractVal(frags[0]); err != nil {
continue
}
if usecTotal, err = extractVal(frags[1]); err != nil {
continue
}

e.metricsMtx.RLock()
e.metrics["command_calls_total"].WithLabelValues(addr, cmd).Set(calls)
e.metrics["command_calls_usec_total"].WithLabelValues(addr, cmd).Set(usecTotal)
e.metricsMtx.RUnlock()
continue
}

if keysTotal, keysEx, avgTTL, ok := parseDBKeyspaceString(split[0], split[1]); ok {
scrapes <- scrapeResult{Name: "db_keys_total", Addr: addr, DB: split[0], Value: keysTotal}
scrapes <- scrapeResult{Name: "db_expiring_keys_total", Addr: addr, DB: split[0], Value: keysEx}
Expand Down Expand Up @@ -340,6 +398,7 @@ func (e *Exporter) scrape(scrapes chan<- scrapeResult) {

errorCount := 0
for idx, addr := range e.redis.Addrs {

c, err := redis.Dial("tcp", addr)
if err != nil {
log.Printf("redis err: %s", err)
Expand All @@ -355,22 +414,26 @@ func (e *Exporter) scrape(scrapes chan<- scrapeResult) {
continue
}
}
info, err := redis.String(c.Do("INFO"))

info, err := redis.String(c.Do("INFO", "ALL"))
if err == nil {
err = extractInfoMetrics(info, addr, scrapes)
err = e.extractInfoMetrics(info, addr, scrapes)
}
if err != nil {
log.Printf("redis err: %s", err)
errorCount++
continue
}

config, err := redis.Strings(c.Do("CONFIG", "GET", "maxmemory"))
if err == nil {
err = extractConfigMetrics(config, addr, scrapes)
}

if err != nil {
log.Printf("redis err: %s", err)
errorCount++
continue
}

for _, k := range e.keys {
Expand Down Expand Up @@ -404,14 +467,15 @@ func (e *Exporter) scrape(scrapes chan<- scrapeResult) {
}

func (e *Exporter) setMetrics(scrapes <-chan scrapeResult) {

for scr := range scrapes {
name := scr.Name
if _, ok := e.metrics[name]; !ok {
e.metricsMtx.Lock()
e.metrics[name] = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: e.namespace,
Name: name,
}, []string{"addr"})
e.metricsMtx.Unlock()
}
var labels prometheus.Labels = map[string]string{"addr": scr.Addr}
if len(scr.DB) > 0 {
Expand Down
81 changes: 58 additions & 23 deletions exporter/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var (
)

const (
TEST_SET_NAME = "test-set"
TestSetName = "test-set"
)

func setupDBKeys(t *testing.T) error {
Expand Down Expand Up @@ -71,8 +71,8 @@ func setupDBKeys(t *testing.T) error {
}
}

c.Do("SADD", TEST_SET_NAME, "test-val-1")
c.Do("SADD", TEST_SET_NAME, "test-val-2")
c.Do("SADD", TestSetName, "test-val-1")
c.Do("SADD", TestSetName, "test-val-2")

time.Sleep(time.Millisecond * 50)

Expand Down Expand Up @@ -102,7 +102,7 @@ func deleteKeysFromDB(t *testing.T) error {
c.Do("DEL", key)
}

c.Do("DEL", TEST_SET_NAME)
c.Do("DEL", TestSetName)

return nil
}
Expand Down Expand Up @@ -135,8 +135,8 @@ func TestCountingKeys(t *testing.T) {

e, _ := NewRedisExporter(r, "test", "")

scrapes := make(chan scrapeResult)
go e.scrape(scrapes)
scrapes := make(chan scrapeResult, 10000)
e.scrape(scrapes)

var keysTestDB float64
for s := range scrapes {
Expand All @@ -149,8 +149,8 @@ func TestCountingKeys(t *testing.T) {
setupDBKeys(t)
defer deleteKeysFromDB(t)

scrapes = make(chan scrapeResult)
go e.scrape(scrapes)
scrapes = make(chan scrapeResult, 1000)
e.scrape(scrapes)

// +1 for the one SET key
want := keysTestDB + float64(len(keys)) + float64(len(keysExpiring)) + 1
Expand All @@ -165,8 +165,8 @@ func TestCountingKeys(t *testing.T) {
}

deleteKeysFromDB(t)
scrapes = make(chan scrapeResult)
go e.scrape(scrapes)
scrapes = make(chan scrapeResult, 10000)
e.scrape(scrapes)

for s := range scrapes {
if s.Name == "db_keys_total" && s.DB == dbNumStrFull {
Expand All @@ -191,8 +191,8 @@ func TestExporterMetrics(t *testing.T) {
setupDBKeys(t)
defer deleteKeysFromDB(t)

scrapes := make(chan scrapeResult)
go e.scrape(scrapes)
scrapes := make(chan scrapeResult, 10000)
e.scrape(scrapes)

e.setMetrics(scrapes)

Expand Down Expand Up @@ -223,8 +223,8 @@ func TestExporterValues(t *testing.T) {
setupDBKeys(t)
defer deleteKeysFromDB(t)

scrapes := make(chan scrapeResult)
go e.scrape(scrapes)
scrapes := make(chan scrapeResult, 10000)
e.scrape(scrapes)

wantValues := map[string]float64{
"db_keys_total": float64(len(keys)+len(keysExpiring)) + 1, // + 1 for the SET key
Expand All @@ -249,17 +249,17 @@ type tstData struct {

func TestKeyspaceStringParser(t *testing.T) {
tsts := []tstData{
tstData{db: "xxx", stats: "", ok: false},
tstData{db: "xxx", stats: "keys=1,expires=0,avg_ttl=0", ok: false},
tstData{db: "db0", stats: "xxx", ok: false},
tstData{db: "db1", stats: "keys=abcd,expires=0,avg_ttl=0", ok: false},
tstData{db: "db2", stats: "keys=1234=1234,expires=0,avg_ttl=0", ok: false},
{db: "xxx", stats: "", ok: false},
{db: "xxx", stats: "keys=1,expires=0,avg_ttl=0", ok: false},
{db: "db0", stats: "xxx", ok: false},
{db: "db1", stats: "keys=abcd,expires=0,avg_ttl=0", ok: false},
{db: "db2", stats: "keys=1234=1234,expires=0,avg_ttl=0", ok: false},

tstData{db: "db3", stats: "keys=abcde,expires=0", ok: false},
tstData{db: "db3", stats: "keys=213,expires=xxx", ok: false},
tstData{db: "db3", stats: "keys=123,expires=0,avg_ttl=zzz", ok: false},
{db: "db3", stats: "keys=abcde,expires=0", ok: false},
{db: "db3", stats: "keys=213,expires=xxx", ok: false},
{db: "db3", stats: "keys=123,expires=0,avg_ttl=zzz", ok: false},

tstData{db: "db0", stats: "keys=1,expires=0,avg_ttl=0", keysTotal: 1, keysEx: 0, avgTTL: 0, ok: true},
{db: "db0", stats: "keys=1,expires=0,avg_ttl=0", keysTotal: 1, keysEx: 0, avgTTL: 0, ok: true},
}

for _, tst := range tsts {
Expand Down Expand Up @@ -312,6 +312,41 @@ func TestKeyValuesAndSizes(t *testing.T) {
}
}

func TestCommandStats(t *testing.T) {

e, _ := NewRedisExporter(r, "test", dbNumStrFull+"="+url.QueryEscape(keys[0]))

setupDBKeys(t)
defer deleteKeysFromDB(t)

chM := make(chan prometheus.Metric)
go func() {
e.Collect(chM)
close(chM)
}()

want := map[string]bool{"test_command_calls_total": false, "test_command_calls_usec_total": false}

for m := range chM {
switch m.(type) {
case prometheus.Gauge:
for k := range want {
if strings.Contains(m.Desc().String(), k) {
want[k] = true
}
}
default:
log.Printf("default: m: %#v", m)
}
}
for k, v := range want {
if !v {
t.Errorf("didn't find %s", k)
}

}
}

func TestHTTPEndpoint(t *testing.T) {

e, _ := NewRedisExporter(r, "test", dbNumStrFull+"="+url.QueryEscape(keys[0]))
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var (
showVersion = flag.Bool("version", false, "Show version information and exit")

// VERSION of Redis Exporter
VERSION = "0.6"
VERSION = "0.7"
)

func main() {
Expand Down

1 comment on commit 1a3254a

@mfouilleul
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one again! I will test it soon.

Please sign in to comment.