diff --git a/exporter/clients.go b/exporter/clients.go index 0748c8aa..a47a9c11 100644 --- a/exporter/clients.go +++ b/exporter/clients.go @@ -2,7 +2,9 @@ package exporter import ( "regexp" + "strconv" "strings" + "time" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" @@ -28,6 +30,18 @@ func parseClientListString(clientInfo string) ([]string, bool) { connectedClient[vPart[0]] = vPart[1] } + createdAtTs, err := durationFieldToTimestamp(connectedClient["age"]) + if err != nil { + log.Debugf("cloud not parse age field(%s): %s", connectedClient["age"], err.Error()) + return nil, false + } + + idleSinceTs, err := durationFieldToTimestamp(connectedClient["idle"]) + if err != nil { + log.Debugf("cloud not parse idle field(%s): %s", connectedClient["idle"], err.Error()) + return nil, false + } + hostPortString := strings.Split(connectedClient["addr"], ":") if len(hostPortString) != 2 { return nil, false @@ -35,8 +49,8 @@ func parseClientListString(clientInfo string) ([]string, bool) { return []string{ connectedClient["name"], - connectedClient["age"], - connectedClient["idle"], + createdAtTs, + idleSinceTs, connectedClient["flags"], connectedClient["db"], connectedClient["omem"], @@ -48,6 +62,15 @@ func parseClientListString(clientInfo string) ([]string, bool) { } +func durationFieldToTimestamp(field string) (string, error) { + parsed, err := strconv.ParseInt(field, 10, 64) + if err != nil { + return "", err + } + + return strconv.FormatInt(time.Now().Unix()-parsed, 10), nil +} + func (e *Exporter) extractConnectedClientMetrics(ch chan<- prometheus.Metric, c redis.Conn) { reply, err := redis.String(doRedisCmd(c, "CLIENT", "LIST")) if err != nil { diff --git a/exporter/clients_test.go b/exporter/clients_test.go index 4ab8cb43..b74e4d9a 100644 --- a/exporter/clients_test.go +++ b/exporter/clients_test.go @@ -1,29 +1,105 @@ package exporter import ( + "strconv" "strings" "testing" + "time" "github.com/prometheus/client_golang/prometheus" ) +func TestDurationFieldToTimestamp(t *testing.T) { + nowTs := time.Now().Unix() + for _, tst := range []struct { + in string + expectedOk bool + expectedVal int64 + }{ + { + in: "123", + expectedOk: true, + expectedVal: nowTs - 123, + }, + { + in: "0", + expectedOk: true, + expectedVal: nowTs - 0, + }, + { + in: "abc", + expectedOk: false, + }, + } { + res, err := durationFieldToTimestamp(tst.in) + if err == nil && !tst.expectedOk { + t.Fatalf("expected not ok, but got no error, input: [%s]", tst.in) + } else if err != nil && tst.expectedOk { + t.Fatalf("expected ok, but got error: %s, input: [%s]", err, tst.in) + } + if tst.expectedOk { + resInt64, err := strconv.ParseInt(res, 10, 64) + if err != nil { + t.Fatalf("ParseInt( %s ) err: %s", res, err) + } + if resInt64 != tst.expectedVal { + t.Fatalf("expected %d, but got: %d", tst.expectedVal, resInt64) + } + } + } +} + func TestParseClientListString(t *testing.T) { - tsts := map[string][]string{ - "id=11 addr=127.0.0.1:63508 fd=8 name= age=6321 idle=6320 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=setex": []string{"", "6321", "6320", "N", "0", "0", "setex", "127.0.0.1", "63508"}, - "id=14 addr=127.0.0.1:64958 fd=9 name=foo age=5 idle=0 flags=N db=1 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client": []string{"foo", "5", "0", "N", "1", "0", "client", "127.0.0.1", "64958"}, + convertDurationToTimestampString := func(duration string) string { + ts, err := durationFieldToTimestamp(duration) + if err != nil { + panic(err) + } + return ts + } + + tsts := []struct { + in string + expectedOk bool + expectedLbls []string + }{ + { + in: "id=11 addr=127.0.0.1:63508 fd=8 name= age=6321 idle=6320 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=setex", + expectedOk: true, + expectedLbls: []string{"", convertDurationToTimestampString("6321"), convertDurationToTimestampString("6320"), "N", "0", "0", "setex", "127.0.0.1", "63508"}, + }, { + in: "id=14 addr=127.0.0.1:64958 fd=9 name=foo age=5 idle=0 flags=N db=1 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client", + expectedOk: true, + expectedLbls: []string{"foo", convertDurationToTimestampString("5"), convertDurationToTimestampString("0"), "N", "1", "0", "client", "127.0.0.1", "64958"}, + }, { + in: "id=14 addr=127.0.0.1:64958 fd=9 name=foo age=ABCDE idle=0 flags=N db=1 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client", + expectedOk: false, + }, { + in: "id=14 addr=127.0.0.1:64958 fd=9 name=foo age=5 idle=NOPE flags=N db=1 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client", + expectedOk: false, + }, { + in: "", + expectedOk: false, + }, } - for k, v := range tsts { - lbls, ok := parseClientListString(k) + for _, tst := range tsts { + lbls, ok := parseClientListString(tst.in) + if !tst.expectedOk { + if ok { + t.Errorf("expected NOT ok, but got ok, input: %s", tst.in) + } + continue + } mismatch := false for idx, l := range lbls { - if l != v[idx] { + if l != tst.expectedLbls[idx] { mismatch = true break } } - if !ok || mismatch { - t.Errorf("TestParseClientListString( %s ) error. Given: %s Wanted: %s", k, lbls, v) + if mismatch { + t.Errorf("TestParseClientListString( %s ) error. Given: %s Wanted: %s", tst.in, lbls, tst.expectedLbls) } } } diff --git a/exporter/exporter.go b/exporter/exporter.go index ff9155cc..3838899e 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -310,7 +310,7 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) { e.metricDescriptions = map[string]*prometheus.Desc{} - connectedClientsLabels := []string{"name", "age", "idle", "flags", "db", "omem", "cmd", "host"} + connectedClientsLabels := []string{"name", "created_at", "idle_since", "flags", "db", "omem", "cmd", "host"} if e.options.ExportClientsInclPort { connectedClientsLabels = append(connectedClientsLabels, "port") } diff --git a/exporter/sentinels_test.go b/exporter/sentinels_test.go index d417e470..1afadbf4 100644 --- a/exporter/sentinels_test.go +++ b/exporter/sentinels_test.go @@ -218,7 +218,7 @@ func TestProcessSentinelSentinels(t *testing.T) { tsts := []sentinelSentinelsData{ {"1/1 okay sentinel", []interface{}{[]interface{}{[]byte("")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue}, {"1/3 okay sentinel", []interface{}{[]interface{}{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("o_down,s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []interface{}{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, oneOkSentinelExpectedMetricValue}, - {"2/3 okay sentinel(string is not byte slice)", []interface{}{[]interface{}{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []interface{}{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), ("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, + {"2/3 okay sentinel(string is not byte slice)", []interface{}{[]interface{}{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []interface{}{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), "sentinel", []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, {"2/3 okay sentinel", []interface{}{[]interface{}{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []interface{}{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("flags"), []byte("s_down,sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, {"2/3 okay sentinel(missing flags)", []interface{}{[]interface{}{[]byte("name"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("ip"), []byte("172.17.0.8"), []byte("port"), []byte("26379"), []byte("runid"), []byte("284bc2ef46881bd71e81610152cb96031d211d28"), []byte("flags"), []byte("sentinel"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823816"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}, []interface{}{[]byte("name"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("ip"), []byte("172.17.0.7"), []byte("port"), []byte("26379"), []byte("runid"), []byte("c3ab3cdcaeb193bb49b16d4d3da88def984ab3bf"), []byte("link-pending-commands"), []byte("38"), []byte("link-refcount"), []byte("1"), []byte("last-ping-sent"), []byte("11828891"), []byte("last-ok-ping-reply"), []byte("11829539"), []byte("last-ping-reply"), []byte("11829539"), []byte("s-down-time"), []byte("11823815"), []byte("down-after-milliseconds"), []byte("5000"), []byte("last-hello-message"), []byte("11829434"), []byte("voted-leader"), []byte("?"), []byte("voted-leader-epoch"), []byte("0")}}, []string{"mymaster", "172.17.0.7:26379"}, twoOkSentinelExpectedMetricValue}, }