Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,28 @@ func TestE2E_GetMulti_missing(t *testing.T) {
must.Eq(t, &Pair[int, error]{A: 3, B: nil}, results[2])
must.ErrorIs(t, ErrCacheMiss, results[1].B)
}

func TestE2E_Stats(t *testing.T) {
t.Parallel()

address, done := memctest.LaunchTCP(t, nil)
t.Cleanup(done)

c := New([]string{address})
defer ignore.Close(c)

// insert an item
err := Set(c, "mykey", "myvalue", TTL(1*time.Hour))
must.NoError(t, err)

s, serr := Stats(c)
must.NoError(t, serr)

// spot check a few fields
must.StrHasPrefix(t, "1.", s.Runtime.Version)
must.Positive(t, s.Runtime.Threads)
must.Positive(t, s.Connections.Max)
must.Positive(t, s.Connections.Current)
must.One(t, s.Items.Current)
must.Eq(t, 71, s.Items.Bytes)
}
169 changes: 169 additions & 0 deletions stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright CattleCloud LLC 2025, 2026
// SPDX-License-Identifier: BSD-3-Clause

package memc

import (
"bufio"
"io"
"strconv"
"strings"
)

type Statistics struct {
Runtime struct {
PID int `json:"pid"`
Uptime int `json:"uptime"`
Time int `json:"time"`
Version string `json:"version"`
LibEvent string `json:"libevent"`
PointerSize int `json:"pointer_size"`
Threads int `json:"threads"`
}

Resources struct {
RUsageUser float64 `json:"rusage_user"`
RUsageSystem float64 `json:"rusage_system"`
}

Connections struct {
Max int `json:"max_connections"`
Current int `json:"curr_connections"`
Total int `json:"total_connections"`
Rejected int `json:"rejected_connections"`
Structures int `json:"connection_structures"`
}

Commands struct {
Get int `json:"cmd_get"`
Set int `json:"cmd_set"`
Flush int `json:"cmd_flush"`
Touch int `json:"cmd_touch"`
Meta int `json:"cmd_meta"`

Hit struct {
Get int `json:"get_hits"`
Delete int `json:"delete_hits"`
Increment int `json:"incr_hits"`
Decrement int `json:"decr_hits"`
Touch int `json:"touch_hits"`
CAS int `json:"cas_hits"`
}

Miss struct {
Get int `json:"get_misses"`
Delete int `json:"delete_misses"`
Increment int `json:"incr_misses"`
Decrement int `json:"decr_misses"`
Touch int `json:"touch_misses"`
CAS int `json:"cas_misses"`
}

Failure struct {
GetExpired int `json:"get_expired"`
GetFlushed int `json:"get_flushed"`
CASBadValue int `json:"cas_badval"`
}
}

Items struct {
Bytes int `json:"bytes"`
Current int `json:"curr_items"`
Total int `json:"total_items"`
}
}

func stats(r io.Reader) (*Statistics, error) {
scanner := bufio.NewScanner(r)
m := make(map[string]string)

// parse the contents of the stats output line by line
for scanner.Scan() {
line := scanner.Text()
if line == "END" {
break
}

fields := strings.Fields(line)
if len(fields) < 3 || fields[0] != "STAT" {
continue
}

// skip fields[0], is STAT
key := fields[1]
value := fields[2]
m[key] = value
}

// make sure the scanner was successful
if err := scanner.Err(); err != nil {
return nil, err
}

s := new(Statistics)

// map Runtime
s.Runtime.PID = toInt(m["pid"])
s.Runtime.Uptime = toInt(m["uptime"])
s.Runtime.Time = toInt(m["time"])
s.Runtime.Version = m["version"]
s.Runtime.LibEvent = m["libevent"]
s.Runtime.PointerSize = toInt(m["pointer_size"])
s.Runtime.Threads = toInt(m["threads"])

// map Resources
s.Resources.RUsageUser = toFloat64(m["rusage_user"])
s.Resources.RUsageSystem = toFloat64(m["rusage_system"])

// map Connections
s.Connections.Max = toInt(m["max_connections"])
s.Connections.Current = toInt(m["curr_connections"])
s.Connections.Total = toInt(m["total_connections"])
s.Connections.Rejected = toInt(m["rejected_connections"])
s.Connections.Structures = toInt(m["connection_structures"])

// map Commands
s.Commands.Get = toInt(m["cmd_get"])
s.Commands.Set = toInt(m["cmd_set"])
s.Commands.Flush = toInt(m["cmd_flush"])
s.Commands.Touch = toInt(m["cmd_touch"])
s.Commands.Meta = toInt(m["cmd_meta"])

// map Hits
s.Commands.Hit.Get = toInt(m["get_hits"])
s.Commands.Hit.Delete = toInt(m["delete_hits"])
s.Commands.Hit.Increment = toInt(m["incr_hits"])
s.Commands.Hit.Decrement = toInt(m["decr_hits"])
s.Commands.Hit.Touch = toInt(m["touch_hits"])
s.Commands.Hit.CAS = toInt(m["cas_hits"])

// map Misses
s.Commands.Miss.Get = toInt(m["get_misses"])
s.Commands.Miss.Delete = toInt(m["delete_misses"])
s.Commands.Miss.Increment = toInt(m["incr_misses"])
s.Commands.Miss.Decrement = toInt(m["decr_misses"])
s.Commands.Miss.Touch = toInt(m["touch_misses"])
s.Commands.Miss.CAS = toInt(m["cas_misses"])

// map Failures
s.Commands.Failure.GetExpired = toInt(m["get_expired"])
s.Commands.Failure.GetFlushed = toInt(m["get_flushed"])
s.Commands.Failure.CASBadValue = toInt(m["cas_badval"])

// map Items
s.Items.Bytes = toInt(m["bytes"])
s.Items.Current = toInt(m["curr_items"])
s.Items.Total = toInt(m["total_items"])

return s, nil
}

func toInt(s string) int {
v, _ := strconv.Atoi(s)
return v
}

func toFloat64(s string) float64 {
v, _ := strconv.ParseFloat(s, 64)
return v
}
119 changes: 119 additions & 0 deletions stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright CattleCloud LLC 2025, 2026
// SPDX-License-Identifier: BSD-3-Clause

package memc

import (
"strings"
"testing"

"github.com/shoenig/test/must"
)

func Test_stats(t *testing.T) {
t.Parallel()

input := strings.NewReader(realStats)
result, err := stats(input)
must.NoError(t, err)

// spot check a few values
must.Eq(t, 714, result.Runtime.PID)
must.Eq(t, 1024, result.Connections.Max)
}

const realStats = `
STAT pid 714
STAT uptime 2077665
STAT time 1769190296
STAT version 1.6.29
STAT libevent 2.1.12-stable
STAT pointer_size 64
STAT rusage_user 551.779188
STAT rusage_system 808.853255
STAT max_connections 1024
STAT curr_connections 1
STAT total_connections 13714
STAT rejected_connections 0
STAT connection_structures 5
STAT response_obj_oom 0
STAT response_obj_count 1
STAT response_obj_bytes 65536
STAT read_buf_count 8
STAT read_buf_bytes 131072
STAT read_buf_bytes_free 49152
STAT read_buf_oom 0
STAT reserved_fds 20
STAT cmd_get 15249
STAT cmd_set 12260
STAT cmd_flush 0
STAT cmd_touch 0
STAT cmd_meta 0
STAT get_hits 2918
STAT get_misses 12331
STAT get_expired 78
STAT get_flushed 0
STAT delete_misses 0
STAT delete_hits 0
STAT incr_misses 0
STAT incr_hits 0
STAT decr_misses 0
STAT decr_hits 0
STAT cas_misses 0
STAT cas_hits 0
STAT cas_badval 0
STAT touch_hits 0
STAT touch_misses 0
STAT store_too_large 0
STAT store_no_memory 0
STAT auth_cmds 0
STAT auth_errors 0
STAT bytes_read 21752597
STAT bytes_written 125490335
STAT limit_maxbytes 2147483648
STAT accepting_conns 1
STAT listen_disabled_num 0
STAT time_in_listen_disabled_us 0
STAT threads 4
STAT conn_yields 0
STAT hash_power_level 16
STAT hash_bytes 524288
STAT hash_is_expanding 0
STAT slab_reassign_rescues 0
STAT slab_reassign_chunk_rescues 0
STAT slab_reassign_evictions_nomem 0
STAT slab_reassign_inline_reclaim 0
STAT slab_reassign_busy_items 0
STAT slab_reassign_busy_deletes 0
STAT slab_reassign_running 0
STAT slabs_moved 0
STAT lru_crawler_running 0
STAT lru_crawler_starts 1989
STAT lru_maintainer_juggles 29456575
STAT malloc_fails 0
STAT log_worker_dropped 0
STAT log_worker_written 0
STAT log_watcher_skipped 0
STAT log_watcher_sent 0
STAT log_watchers 0
STAT unexpected_napi_ids 0
STAT round_robin_fallback 0
STAT bytes 51321
STAT curr_items 11
STAT total_items 12260
STAT slab_global_page_pool 0
STAT expired_unfetched 11528
STAT evicted_unfetched 0
STAT evicted_active 0
STAT evictions 0
STAT reclaimed 11942
STAT crawler_reclaimed 130
STAT crawler_items_checked 3792
STAT lrutail_reflocked 619
STAT moves_to_cold 13354
STAT moves_to_warm 1368
STAT moves_within_lru 146
STAT direct_reclaims 0
STAT lru_bumps_dropped 0
END
`
27 changes: 27 additions & 0 deletions verbs.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,33 @@ func Decrement[T Countable](c *Client, key string, delta T) (T, error) {
return result, err
}

func Stats(c *Client) (*Statistics, error) {
var statistics *Statistics

err := c.do("", func(conn *iopool.Buffer) error {
// write the header component
if _, err := fmt.Fprintf(conn, "stats\r\n"); err != nil {
return err
}

// flush the connection, forcing bytes over the wire
if err := conn.Flush(); err != nil {
return err
}

// extract the statistics payload
payload, perr := stats(conn.Reader)
if perr != nil {
return perr
}
statistics = payload

return nil
})

return statistics, err
}

func unexpected(response []byte) error {
return fmt.Errorf(
"unexpected response from memcached %q",
Expand Down