Skip to content

Commit

Permalink
include user info in redis_connected_clients_details (#776)
Browse files Browse the repository at this point in the history
* user in client info
  • Loading branch information
misTrasteos authored Mar 18, 2023
1 parent 3793ea4 commit de4f5eb
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 55 deletions.
113 changes: 75 additions & 38 deletions exporter/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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...,
)
}
}
Expand Down
49 changes: 38 additions & 11 deletions exporter/clients_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exporter

import (
"os"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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`)
}
}
6 changes: 0 additions & 6 deletions exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"}},
Expand Down
5 changes: 5 additions & 0 deletions exporter/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit de4f5eb

Please sign in to comment.