From de4f5eb0bfc3e15e3aa882db262572ed3bb45dcb Mon Sep 17 00:00:00 2001 From: Hefesto <5575151+misTrasteos@users.noreply.github.com> Date: Sat, 18 Mar 2023 01:46:40 +0100 Subject: [PATCH] include user info in redis_connected_clients_details (#776) * user in client info --- exporter/clients.go | 113 +++++++++++++++++++++++++------------- exporter/clients_test.go | 49 +++++++++++++---- exporter/exporter.go | 6 -- exporter/exporter_test.go | 5 ++ 4 files changed, 118 insertions(+), 55 deletions(-) diff --git a/exporter/clients.go b/exporter/clients.go index a47a9c11..fff959e3 100644 --- a/exporter/clients.go +++ b/exporter/clients.go @@ -11,55 +11,77 @@ import ( log "github.com/sirupsen/logrus" ) +type ClientInfo struct { + Name, + User, + CreatedAt, + IdleSince, + Flags, + Db, + OMem, + Cmd, + Host, + Port, + Resp string +} + /* Valid Examples - 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 - id=14 addr=127.0.0.1:64958 fd=9 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client + 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 user=default resp=2 + id=14 addr=127.0.0.1:64958 fd=9 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client user=default resp=3 */ -func parseClientListString(clientInfo string) ([]string, bool) { +func parseClientListString(clientInfo string) (*ClientInfo, bool) { if matched, _ := regexp.MatchString(`^id=\d+ addr=\d+`, clientInfo); !matched { return nil, false } - connectedClient := map[string]string{} + connectedClient := ClientInfo{} for _, kvPart := range strings.Split(clientInfo, " ") { vPart := strings.Split(kvPart, "=") if len(vPart) != 2 { log.Debugf("Invalid format for client list string, got: %s", kvPart) return nil, false } - 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 + switch vPart[0] { + case "name": + connectedClient.Name = vPart[1] + case "user": + connectedClient.User = vPart[1] + case "age": + createdAt, err := durationFieldToTimestamp(vPart[1]) + if err != nil { + log.Debugf("cloud not parse age field(%s): %s", vPart[1], err.Error()) + return nil, false + } + connectedClient.CreatedAt = createdAt + case "idle": + idleSinceTs, err := durationFieldToTimestamp(vPart[1]) + if err != nil { + log.Debugf("cloud not parse idle field(%s): %s", vPart[1], err.Error()) + return nil, false + } + connectedClient.IdleSince = idleSinceTs + case "flags": + connectedClient.Flags = vPart[1] + case "db": + connectedClient.Db = vPart[1] + case "omem": + connectedClient.OMem = vPart[1] + case "cmd": + connectedClient.Cmd = vPart[1] + case "addr": + hostPortString := strings.Split(vPart[1], ":") + if len(hostPortString) != 2 { + return nil, false + } + connectedClient.Host = hostPortString[0] + connectedClient.Port = hostPortString[1] + case "resp": + connectedClient.Resp = vPart[1] + } } - return []string{ - connectedClient["name"], - createdAtTs, - idleSinceTs, - connectedClient["flags"], - connectedClient["db"], - connectedClient["omem"], - connectedClient["cmd"], - - hostPortString[0], // host - hostPortString[1], // port - }, true - + return &connectedClient, true } func durationFieldToTimestamp(field string) (string, error) { @@ -80,14 +102,29 @@ func (e *Exporter) extractConnectedClientMetrics(ch chan<- prometheus.Metric, c for _, c := range strings.Split(reply, "\n") { if lbls, ok := parseClientListString(c); ok { + connectedClientsLabels := []string{"name", "created_at", "idle_since", "flags", "db", "omem", "cmd", "host"} + connectedClientsLabelsValues := []string{lbls.Name, lbls.CreatedAt, lbls.IdleSince, lbls.Flags, lbls.Db, lbls.OMem, lbls.Cmd, lbls.Host} - // port is the last item, we'll trim it if it's not needed - if !e.options.ExportClientsInclPort { - lbls = lbls[:len(lbls)-1] + if e.options.ExportClientsInclPort { + connectedClientsLabels = append(connectedClientsLabels, "port") + connectedClientsLabelsValues = append(connectedClientsLabelsValues, lbls.Port) } + + if user := lbls.User; user != "" { + connectedClientsLabels = append(connectedClientsLabels, "user") + connectedClientsLabelsValues = append(connectedClientsLabelsValues, user) + } + + if resp := lbls.Resp; resp != "" { + connectedClientsLabels = append(connectedClientsLabels, "resp") + connectedClientsLabelsValues = append(connectedClientsLabelsValues, resp) + } + + e.metricDescriptions["connected_clients_details"] = newMetricDescr(e.options.Namespace, "connected_clients_details", "Details about connected clients", connectedClientsLabels) + e.registerConstMetricGauge( ch, "connected_clients_details", 1.0, - lbls..., + connectedClientsLabelsValues..., ) } } diff --git a/exporter/clients_test.go b/exporter/clients_test.go index b74e4d9a..fc88eb9b 100644 --- a/exporter/clients_test.go +++ b/exporter/clients_test.go @@ -1,6 +1,7 @@ package exporter import ( + "os" "strconv" "strings" "testing" @@ -61,16 +62,20 @@ func TestParseClientListString(t *testing.T) { tsts := []struct { in string expectedOk bool - expectedLbls []string + expectedLbls ClientInfo }{ { 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"}, + expectedLbls: ClientInfo{CreatedAt: convertDurationToTimestampString("6321"), IdleSince: convertDurationToTimestampString("6320"), Flags: "N", Db: "0", OMem: "0", Cmd: "setex", Host: "127.0.0.1", Port: "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"}, + expectedLbls: ClientInfo{Name: "foo", CreatedAt: convertDurationToTimestampString("5"), IdleSince: convertDurationToTimestampString("0"), Flags: "N", Db: "1", OMem: "0", Cmd: "client", Host: "127.0.0.1", Port: "64958"}, + }, { + in: "id=14 addr=127.0.0.1:64959 fd=9 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client user=default resp=3", + expectedOk: true, + expectedLbls: ClientInfo{CreatedAt: convertDurationToTimestampString("5"), IdleSince: convertDurationToTimestampString("0"), Flags: "N", Db: "0", OMem: "0", Cmd: "client", Host: "127.0.0.1", Port: "64959", User: "default", Resp: "3"}, }, { 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, @@ -91,14 +96,8 @@ func TestParseClientListString(t *testing.T) { } continue } - mismatch := false - for idx, l := range lbls { - if l != tst.expectedLbls[idx] { - mismatch = true - break - } - } - if mismatch { + + if *lbls != tst.expectedLbls { t.Errorf("TestParseClientListString( %s ) error. Given: %s Wanted: %s", tst.in, lbls, tst.expectedLbls) } } @@ -163,3 +162,31 @@ func TestExportClientListInclPort(t *testing.T) { } } } + +func TestExportClientListResp(t *testing.T) { + redisSevenAddr := os.Getenv("TEST_REDIS7_URI") + e := getTestExporterWithAddrAndOptions(redisSevenAddr, Options{ + Namespace: "test", Registry: prometheus.NewRegistry(), + ExportClientList: true, + }) + + chM := make(chan prometheus.Metric) + go func() { + e.Collect(chM) + close(chM) + }() + + found := false + for m := range chM { + desc := m.Desc().String() + if strings.Contains(desc, "connected_clients_details") { + if strings.Contains(desc, "resp") { + found = true + } + } + } + + if !found { + t.Errorf(`connected_clients_details did *not* include "resp" in isExportClientList metrics but was expected`) + } +} diff --git a/exporter/exporter.go b/exporter/exporter.go index bdffee8c..d8864985 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -334,11 +334,6 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) { e.metricDescriptions = map[string]*prometheus.Desc{} - connectedClientsLabels := []string{"name", "created_at", "idle_since", "flags", "db", "omem", "cmd", "host"} - if e.options.ExportClientsInclPort { - connectedClientsLabels = append(connectedClientsLabels, "port") - } - for k, desc := range map[string]struct { txt string lbls []string @@ -351,7 +346,6 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) { "latency_percentiles_usec": {txt: `A summary of latency percentile distribution per command`, lbls: []string{"cmd"}}, "config_key_value": {txt: `Config key and value`, lbls: []string{"key", "value"}}, "config_value": {txt: `Config key and value as metric`, lbls: []string{"key"}}, - "connected_clients_details": {txt: "Details about connected clients", lbls: connectedClientsLabels}, "connected_slave_lag_seconds": {txt: "Lag of connected slave", lbls: []string{"slave_ip", "slave_port", "slave_state"}}, "connected_slave_offset_bytes": {txt: "Offset of connected slave", lbls: []string{"slave_ip", "slave_port", "slave_state"}}, "db_avg_ttl_seconds": {txt: "Avg TTL in seconds", lbls: []string{"db"}}, diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go index dc31922e..e34452e2 100644 --- a/exporter/exporter_test.go +++ b/exporter/exporter_test.go @@ -70,6 +70,11 @@ func getTestExporterWithAddr(addr string) *Exporter { return e } +func getTestExporterWithAddrAndOptions(addr string, opt Options) *Exporter { + e, _ := NewRedisExporter(addr, opt) + return e +} + func setupKeys(t *testing.T, c redis.Conn, dbNumStr string) error { if _, err := c.Do("SELECT", dbNumStr); err != nil { log.Printf("setupDBKeys() - couldn't setup redis, err: %s ", err)