Skip to content

Commit

Permalink
Re-implement connected_client to have deterministic cardinality (#950)
Browse files Browse the repository at this point in the history
* Re-implement connected_client to have deterministic cardinality

* More tests, remove backwards compatibility metrics

* simplify int parsing, update tests

Signed-off-by: Gabi Davar <grizzly.nyo@gmail.com>

---------

Signed-off-by: Gabi Davar <grizzly.nyo@gmail.com>
  • Loading branch information
mindw authored Oct 5, 2024
1 parent 1f06f2f commit f1bafc8
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 62 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test:
TEST_REDIS_URI="redis://localhost:16384" \
TEST_REDIS5_URI="redis://localhost:16383" \
TEST_REDIS6_URI="redis://localhost:16379" \
TEST_REDIS74_URI="redis://localhost:16385" \
TEST_VALKEY7_URI="redis://localhost:16384" \
TEST_REDIS_2_8_URI="redis://localhost:16381" \
TEST_KEYDB01_URI="redis://localhost:16401" \
Expand Down
7 changes: 6 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:

valkey7:
Expand All @@ -20,6 +19,12 @@ services:
ports:
- "16379:6379"

redis74:
image: redis:7.4
command: "redis-server --protected-mode no --dbfilename dump74.rdb"
ports:
- "16385:6379"

pwd-redis5:
image: redis:5
command: "redis-server --requirepass redis-password --dbfilename dump5-pwd.rdb"
Expand Down
225 changes: 198 additions & 27 deletions exporter/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@ import (
)

type ClientInfo struct {
Id,
Name,
User,
CreatedAt,
IdleSince,
Flags,
Db,
OMem,
Cmd,
Host,
Port,
Resp string
CreatedAt,
IdleSince,
Sub,
Psub,
Ssub,
Watch,
Qbuf,
QbufFree,
Obl,
Oll,
OMem,
TotMem int64
}

/*
Expand All @@ -36,6 +45,8 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) {
return nil, false
}
connectedClient := ClientInfo{}
connectedClient.Ssub = -1 // mark it as missing - introduced in Redis 7.0.3
connectedClient.Watch = -1 // mark it as missing - introduced in Redis 7.4
for _, kvPart := range strings.Split(clientInfo, " ") {
vPart := strings.Split(kvPart, "=")
if len(vPart) != 2 {
Expand All @@ -44,32 +55,50 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) {
}

switch vPart[0] {
case "id":
connectedClient.Id = vPart[1]
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())
log.Debugf("could 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())
log.Debugf("could 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 "sub":
connectedClient.Sub, _ = strconv.ParseInt(vPart[1], 10, 64)
case "psub":
connectedClient.Psub, _ = strconv.ParseInt(vPart[1], 10, 64)
case "ssub":
connectedClient.Ssub, _ = strconv.ParseInt(vPart[1], 10, 64)
case "watch":
connectedClient.Watch, _ = strconv.ParseInt(vPart[1], 10, 64)
case "qbuf":
connectedClient.Qbuf, _ = strconv.ParseInt(vPart[1], 10, 64)
case "qbuf-free":
connectedClient.QbufFree, _ = strconv.ParseInt(vPart[1], 10, 64)
case "obl":
connectedClient.Obl, _ = strconv.ParseInt(vPart[1], 10, 64)
case "oll":
connectedClient.Oll, _ = strconv.ParseInt(vPart[1], 10, 64)
case "omem":
connectedClient.OMem = vPart[1]
case "cmd":
connectedClient.Cmd = vPart[1]
connectedClient.OMem, _ = strconv.ParseInt(vPart[1], 10, 64)
case "tot-mem":
connectedClient.TotMem, _ = strconv.ParseInt(vPart[1], 10, 64)
case "addr":
hostPortString := strings.Split(vPart[1], ":")
if len(hostPortString) < 2 {
Expand All @@ -86,13 +115,12 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) {
return &connectedClient, true
}

func durationFieldToTimestamp(field string) (string, error) {
func durationFieldToTimestamp(field string) (int64, error) {
parsed, err := strconv.ParseInt(field, 10, 64)
if err != nil {
return "", err
return 0, err
}

return strconv.FormatInt(time.Now().Unix()-parsed, 10), nil
return time.Now().Unix() - parsed, nil
}

func (e *Exporter) extractConnectedClientMetrics(ch chan<- prometheus.Metric, c redis.Conn) {
Expand All @@ -103,30 +131,173 @@ 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}
if info, ok := parseClientListString(c); ok {
clientInfoLabels := []string{"id", "name", "flags", "db", "host"}
clientInfoLabelsValues := []string{info.Id, info.Name, info.Flags, info.Db, info.Host}

if e.options.ExportClientsInclPort {
connectedClientsLabels = append(connectedClientsLabels, "port")
connectedClientsLabelsValues = append(connectedClientsLabelsValues, lbls.Port)
clientInfoLabels = append(clientInfoLabels, "port")
clientInfoLabelsValues = append(clientInfoLabelsValues, info.Port)
}

if user := lbls.User; user != "" {
connectedClientsLabels = append(connectedClientsLabels, "user")
connectedClientsLabelsValues = append(connectedClientsLabelsValues, user)
if user := info.User; user != "" {
clientInfoLabels = append(clientInfoLabels, "user")
clientInfoLabelsValues = append(clientInfoLabelsValues, user)
}

if resp := lbls.Resp; resp != "" {
connectedClientsLabels = append(connectedClientsLabels, "resp")
connectedClientsLabelsValues = append(connectedClientsLabelsValues, resp)
// introduced in Redis 7.0
if resp := info.Resp; resp != "" {
clientInfoLabels = append(clientInfoLabels, "resp")
clientInfoLabelsValues = append(clientInfoLabelsValues, resp)
}

e.metricDescriptions["connected_clients_details"] = newMetricDescr(e.options.Namespace, "connected_clients_details", "Details about connected clients", connectedClientsLabels)
e.metricDescriptions["connected_client_info"] = newMetricDescr(
e.options.Namespace,
"connected_client_info",
"Details about a connected client",
clientInfoLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_info", 1.0,
clientInfoLabelsValues...,
)

clientBaseLabels := []string{"id", "name"}
clientBaseLabelsValues := []string{info.Id, info.Name}

e.metricDescriptions["connected_client_output_buffer_memory_usage_bytes"] = newMetricDescr(
e.options.Namespace,
"connected_client_output_buffer_memory_usage_bytes",
"A connected client's output buffer memory usage in bytes",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_output_buffer_memory_usage_bytes", float64(info.OMem),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_total_memory_consumed_bytes"] = newMetricDescr(
e.options.Namespace,
"connected_client_total_memory_consumed_bytes",
"Total memory consumed by a client in its various buffers",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_total_memory_consumed_bytes", float64(info.TotMem),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_created_at_timestamp"] = newMetricDescr(
e.options.Namespace,
"connected_client_created_at_timestamp",
"A connected client's creation timestamp",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_created_at_timestamp", float64(info.CreatedAt),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_idle_since_timestamp"] = newMetricDescr(
e.options.Namespace,
"connected_client_idle_since_timestamp",
"A connected client's idle since timestamp",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_idle_since_timestamp", float64(info.IdleSince),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_channel_subscriptions_count"] = newMetricDescr(
e.options.Namespace,
"connected_client_channel_subscriptions_count",
"A connected client's number of channel subscriptions",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_channel_subscriptions_count", float64(info.Sub),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_pattern_matching_subscriptions_count"] = newMetricDescr(
e.options.Namespace,
"connected_client_pattern_matching_subscriptions_count",
"A connected client's number of pattern matching subscriptions",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_pattern_matching_subscriptions_count", float64(info.Psub),
clientBaseLabelsValues...,
)

if info.Ssub != -1 {
e.metricDescriptions["connected_client_shard_channel_subscriptions_count"] = newMetricDescr(
e.options.Namespace,
"connected_client_shard_channel_subscriptions_count",
"a connected client's number of shard channel subscriptions",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_shard_channel_subscriptions_count", float64(info.Ssub),
clientBaseLabelsValues...,
)
}
if info.Watch != -1 {
e.metricDescriptions["connected_client_shard_channel_watched_keys"] = newMetricDescr(
e.options.Namespace,
"connected_client_shard_channel_watched_keys",
"a connected client's number of keys it's currently watching",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_shard_channel_watched_keys", float64(info.Watch),
clientBaseLabelsValues...,
)
}

e.metricDescriptions["connected_client_query_buffer_length_bytes"] = newMetricDescr(
e.options.Namespace,
"connected_client_query_buffer_length_bytes",
"A connected client's query buffer length in bytes (0 means no query pending)",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_query_buffer_length_bytes", float64(info.Qbuf),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_query_buffer_free_space_bytes"] = newMetricDescr(
e.options.Namespace,
"connected_client_query_buffer_free_space_bytes",
"A connected client's free space of the query buffer in bytes (0 means the buffer is full)",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_query_buffer_free_space_bytes", float64(info.QbufFree),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_output_buffer_length_bytes"] = newMetricDescr(
e.options.Namespace,
"connected_client_output_buffer_length_bytes",
"A connected client's output buffer length in bytes",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_client_output_buffer_length_bytes", float64(info.Obl),
clientBaseLabelsValues...,
)

e.metricDescriptions["connected_client_output_list_length"] = newMetricDescr(
e.options.Namespace,
"connected_client_output_list_length",
"A connected client's output list length (replies are queued in this list when the buffer is full)",
clientBaseLabels,
)
e.registerConstMetricGauge(
ch, "connected_clients_details", 1.0,
connectedClientsLabelsValues...,
ch, "connected_client_output_list_length", float64(info.Oll),
clientBaseLabelsValues...,
)
}
}
Expand Down
Loading

0 comments on commit f1bafc8

Please sign in to comment.