├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _tools ├── releng └── upload_artifacts ├── cmd └── go-memcached-tool │ └── main.go ├── memdtool.go ├── memdtool_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-memcached-tool 2 | .* 3 | !.gitignore 4 | !.travis.yml 5 | dist/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | script: 5 | - make lint 6 | - make test 7 | after_script: 8 | - make cover 9 | before_deploy: 10 | - make crossbuild 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.1.0](https://github.com/Songmu/go-memcached-tool/compare/86833395...v0.1.0) (2017-12-03) 4 | 5 | * initial release [#2](https://github.com/Songmu/go-memcached-tool/pull/2) ([Songmu](https://github.com/Songmu)) 6 | * implement dump mode [#1](https://github.com/Songmu/go-memcached-tool/pull/1) ([Songmu](https://github.com/Songmu)) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Songmu 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_REVISION = $(shell git rev-parse --short HEAD) 2 | BUILD_LDFLAGS = "-X github.com/Songmu/go-memcached-tool.revision=$(CURRENT_REVISION)" 3 | ifdef update 4 | u=-u 5 | endif 6 | 7 | deps: 8 | go get -d -v ./... 9 | 10 | test-deps: 11 | go get -d -v -t ./... 12 | 13 | devel-deps: deps 14 | go get ${u} golang.org/x/lint/golint 15 | go get ${u} github.com/mattn/goveralls 16 | go get ${u} github.com/motemen/gobump 17 | go get ${u} github.com/laher/goxc 18 | go get ${u} github.com/Songmu/ghch 19 | 20 | test: test-deps 21 | go test 22 | 23 | lint: devel-deps 24 | go vet 25 | golint -set_exit_status 26 | 27 | cover: devel-deps 28 | goveralls 29 | 30 | build: deps 31 | go build -ldflags=$(BUILD_LDFLAGS) ./cmd/go-memcached-tool 32 | 33 | crossbuild: devel-deps 34 | goxc -pv=v$(shell gobump show -r) -build-ldflags=$(BUILD_LDFLAGS) \ 35 | -d=./dist -arch=amd64 -os=linux,darwin,windows \ 36 | -tasks=clean-destination,xc,archive,rmbin 37 | 38 | release: 39 | _tools/releng 40 | _tools/upload_artifacts 41 | 42 | .PHONY: test deps test-deps devel-deps lint cover crossbuild release 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-memcached-tool 2 | ======= 3 | 4 | [![Build Status](https://travis-ci.org/Songmu/go-memcached-tool.png?branch=master)][travis] 5 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license] 6 | [![GoDoc](https://godoc.org/github.com/Songmu/go-memcached-tool?status.svg)][godoc] 7 | 8 | [travis]: https://travis-ci.org/Songmu/go-memcached-tool 9 | [coveralls]: https://coveralls.io/r/Songmu/go-memcached-tool?branch=master 10 | [license]: https://github.com/Songmu/go-memcached-tool/blob/master/LICENSE 11 | [godoc]: https://godoc.org/github.com/Songmu/go-memcached-tool 12 | 13 | ## Description 14 | 15 | go porting from [memcached-tool](https://github.com/memcached/memcached/blob/master/scripts/memcached-tool) in Perl (only support display and dump mode) 16 | 17 | ## Installation 18 | 19 | % go get github.com/Songmu/go-memcached-tool/cmd/go-memcached-tool 20 | 21 | ## Synopsis 22 | 23 | % go-memcached-tool 24 | 25 | ## Author 26 | 27 | [Songmu](https://github.com/Songmu) 28 | -------------------------------------------------------------------------------- /_tools/releng: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo current version: $(gobump show -r) 5 | read -p "input next version: " next_version 6 | 7 | gobump set $next_version -w 8 | ghch -w -N v$next_version 9 | 10 | git ci -am "Checking in changes prior to tagging of version v$next_version" 11 | git tag v$next_version 12 | git push && git push --tags 13 | -------------------------------------------------------------------------------- /_tools/upload_artifacts: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ver=v$(gobump show -r) 5 | make crossbuild 6 | ghr $ver dist/$ver 7 | -------------------------------------------------------------------------------- /cmd/go-memcached-tool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Songmu/go-memcached-tool" 7 | ) 8 | 9 | func main() { 10 | os.Exit((&memdtool.CLI{ErrStream: os.Stderr, OutStream: os.Stdout}).Run(os.Args[1:])) 11 | } 12 | -------------------------------------------------------------------------------- /memdtool.go: -------------------------------------------------------------------------------- 1 | package memdtool 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | const ( 18 | exitCodeOK = iota 19 | exitCodeParseFlagErr 20 | exitCodeErr 21 | ) 22 | 23 | // CLI is struct for command line tool 24 | type CLI struct { 25 | OutStream, ErrStream io.Writer 26 | } 27 | 28 | var helpReg = regexp.MustCompile(`^--?h(?:elp)?$`) 29 | 30 | // Run the memdtool 31 | func (cli *CLI) Run(argv []string) int { 32 | log.SetOutput(cli.ErrStream) 33 | log.SetFlags(0) 34 | 35 | mode := "display" 36 | addr := "127.0.0.1:11211" 37 | if len(argv) > 0 { 38 | modeCandidate := argv[len(argv)-1] 39 | if modeCandidate == "display" || modeCandidate == "dump" { 40 | mode = modeCandidate 41 | argv = argv[:len(argv)-1] 42 | } 43 | if len(argv) > 0 { 44 | addr = argv[0] 45 | if helpReg.MatchString(addr) { 46 | printHelp(cli.ErrStream) 47 | return exitCodeOK 48 | } 49 | } 50 | } 51 | 52 | var proto = "tcp" 53 | if strings.Contains(addr, "/") { 54 | proto = "unix" 55 | } 56 | conn, err := net.Dial(proto, addr) 57 | if err != nil { 58 | log.Println(err.Error()) 59 | return exitCodeErr 60 | } 61 | defer conn.Close() 62 | 63 | switch mode { 64 | case "display": 65 | return cli.display(conn) 66 | case "dump": 67 | return cli.dump(conn) 68 | } 69 | return exitCodeErr 70 | } 71 | 72 | func (cli *CLI) display(conn io.ReadWriter) int { 73 | items, err := GetSlabStats(conn) 74 | if err != nil { 75 | log.Println(err.Error()) 76 | return exitCodeErr 77 | } 78 | 79 | fmt.Fprint(cli.OutStream, " # Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM\n") 80 | for _, ss := range items { 81 | if ss.TotalPages == 0 { 82 | continue 83 | } 84 | size := fmt.Sprintf("%dB", ss.ChunkSize) 85 | if ss.ChunkSize > 1024 { 86 | size = fmt.Sprintf("%.1fK", float64(ss.ChunkSize)/1024.0) 87 | } 88 | full := "no" 89 | if ss.FreeChunksEnd == 0 { 90 | full = "yes" 91 | } 92 | fmt.Fprintf(cli.OutStream, 93 | "%3d %8s %9ds %7d %7d %7s %8d %8d %4d\n", 94 | ss.ID, 95 | size, 96 | ss.Age, 97 | ss.TotalPages, 98 | ss.Number, 99 | full, 100 | ss.Evicted, 101 | ss.EvictedTime, 102 | ss.Outofmemory, 103 | ) 104 | } 105 | return exitCodeOK 106 | } 107 | 108 | func (cli *CLI) dump(conn io.ReadWriter) int { 109 | fmt.Fprint(conn, "stats items\r\n") 110 | slabItems := make(map[string]uint64) 111 | rdr := bufio.NewReader(conn) 112 | for { 113 | lineBytes, _, err := rdr.ReadLine() 114 | if err != nil { 115 | log.Println(err.Error()) 116 | return exitCodeErr 117 | } 118 | line := string(lineBytes) 119 | if line == "END" { 120 | break 121 | } 122 | // ex. STAT items:1:number 1 123 | if !strings.Contains(line, ":number ") { 124 | continue 125 | } 126 | fields := strings.Fields(line) 127 | if len(fields) != 3 { 128 | log.Printf("result of `stats items` is strange: %s\n", line) 129 | return exitCodeErr 130 | } 131 | fields2 := strings.Split(fields[1], ":") 132 | if len(fields2) != 3 { 133 | log.Printf("result of `stats items` is strange: %s\n", line) 134 | return exitCodeErr 135 | } 136 | value, _ := strconv.ParseUint(fields[2], 10, 64) 137 | slabItems[fields2[1]] = value 138 | } 139 | 140 | var totalItems uint64 141 | for _, v := range slabItems { 142 | totalItems += v 143 | } 144 | fmt.Fprintf(cli.ErrStream, "Dumping memcache contents\n") 145 | fmt.Fprintf(cli.ErrStream, " Number of buckets: %d\n", len(slabItems)) 146 | fmt.Fprintf(cli.ErrStream, " Number of items : %d\n", totalItems) 147 | 148 | for k, v := range slabItems { 149 | fmt.Fprintf(cli.ErrStream, "Dumping bucket %s - %d total items\n", k, v) 150 | 151 | keyexp := make(map[string]string, int(v)) 152 | fmt.Fprintf(conn, "stats cachedump %s %d\r\n", k, v) 153 | for { 154 | lineBytes, _, err := rdr.ReadLine() 155 | if err != nil { 156 | log.Println(err.Error()) 157 | return exitCodeErr 158 | } 159 | line := string(lineBytes) 160 | if line == "END" { 161 | break 162 | } 163 | // return format like this 164 | // ITEM piyo [1 b; 1483953061 s] 165 | fields := strings.Fields(line) 166 | if len(fields) == 6 && fields[0] == "ITEM" { 167 | keyexp[fields[1]] = fields[4] 168 | } 169 | } 170 | 171 | for cachekey, exp := range keyexp { 172 | fmt.Fprintf(conn, "get %s\r\n", cachekey) 173 | for { 174 | lineBytes, _, err := rdr.ReadLine() 175 | if err != nil { 176 | log.Println(err.Error()) 177 | return exitCodeErr 178 | } 179 | line := string(lineBytes) 180 | if line == "END" { 181 | break 182 | } 183 | // VALUE hoge 0 6 184 | // hogege 185 | fields := strings.Fields(line) 186 | if len(fields) != 4 || fields[0] != "VALUE" { 187 | continue 188 | } 189 | flags := fields[2] 190 | sizeStr := fields[3] 191 | size, _ := strconv.Atoi(sizeStr) 192 | buf := make([]byte, size) 193 | _, err = rdr.Read(buf) 194 | if err != nil { 195 | log.Println(err.Error()) 196 | return exitCodeErr 197 | } 198 | fmt.Fprintf(cli.OutStream, "add %s %s %s %s\r\n%s\r\n", cachekey, flags, exp, sizeStr, string(buf)) 199 | rdr.ReadLine() 200 | } 201 | } 202 | } 203 | return exitCodeOK 204 | } 205 | 206 | func printHelp(w io.Writer) { 207 | fmt.Fprintf(w, `Usage: memcached-tool [mode] 208 | 209 | memcached-tool 127.0.0.1:11211 display # shows slabs 210 | memcached-tool 127.0.0.1:11211 # same. (default is display) 211 | memcached-tool 127.0.0.1:11211 dump # dump keys and values 212 | 213 | Version: %s (rev: %s) 214 | `, version, revision) 215 | } 216 | 217 | // SlabStat represents slab statuses 218 | type SlabStat struct { 219 | ID uint64 220 | Number uint64 // Count? 221 | Age uint64 222 | Evicted uint64 223 | EvictedNonzero uint64 224 | EvictedTime uint64 225 | Outofmemory uint64 226 | Reclaimed uint64 227 | ChunkSize uint64 228 | ChunksPerPage uint64 229 | TotalPages uint64 230 | TotalChunks uint64 231 | UsedChunks uint64 232 | FreeChunks uint64 233 | FreeChunksEnd uint64 234 | } 235 | 236 | // GetSlabStats takes SlabStats from connection 237 | func GetSlabStats(conn io.ReadWriter) ([]*SlabStat, error) { 238 | retMap := make(map[int]*SlabStat) 239 | fmt.Fprint(conn, "stats items\r\n") 240 | scr := bufio.NewScanner(bufio.NewReader(conn)) 241 | for scr.Scan() { 242 | // ex. STAT items:1:number 1 243 | line := scr.Text() 244 | if line == "END" { 245 | break 246 | } 247 | fields := strings.Fields(line) 248 | if len(fields) != 3 { 249 | return nil, fmt.Errorf("result of `stats items` is strange: %s", line) 250 | } 251 | fields2 := strings.Split(fields[1], ":") 252 | if len(fields2) != 3 { 253 | return nil, fmt.Errorf("result of `stats items` is strange: %s", line) 254 | } 255 | key := fields2[2] 256 | slabNum, _ := strconv.ParseUint(fields2[1], 10, 64) 257 | value, _ := strconv.ParseUint(fields[2], 10, 64) 258 | ss, ok := retMap[int(slabNum)] 259 | if !ok { 260 | ss = &SlabStat{ID: slabNum} 261 | retMap[int(slabNum)] = ss 262 | } 263 | switch key { 264 | case "number": 265 | ss.Number = value 266 | case "age": 267 | ss.Age = value 268 | case "evicted": 269 | ss.Evicted = value 270 | case "evicted_nonzero": 271 | ss.EvictedNonzero = value 272 | case "evicted_time": 273 | ss.EvictedNonzero = value 274 | case "outofmemory": 275 | ss.Outofmemory = value 276 | case "reclaimed": 277 | ss.Reclaimed = value 278 | } 279 | } 280 | if err := scr.Err(); err != nil { 281 | return nil, errors.Wrap(err, "failed to GetSlabStats while scaning stats items") 282 | } 283 | 284 | fmt.Fprint(conn, "stats slabs\r\n") 285 | for scr.Scan() { 286 | // ex. STAT 1:chunk_size 96 287 | line := scr.Text() 288 | if line == "END" { 289 | break 290 | } 291 | fields := strings.Fields(line) 292 | if len(fields) != 3 { 293 | return nil, fmt.Errorf("result of `stats slabs` is strange: %s", line) 294 | } 295 | fields2 := strings.Split(fields[1], ":") 296 | if len(fields2) != 2 { 297 | continue 298 | } 299 | key := fields2[1] 300 | slabNum, _ := strconv.ParseUint(fields2[0], 10, 64) 301 | value, _ := strconv.ParseUint(fields[2], 10, 64) 302 | ss, ok := retMap[int(slabNum)] 303 | if !ok { 304 | ss = &SlabStat{} 305 | retMap[int(slabNum)] = ss 306 | } 307 | 308 | switch key { 309 | case "chunk_size": 310 | ss.ChunkSize = value 311 | case "chunks_per_page": 312 | ss.ChunksPerPage = value 313 | case "total_pages": 314 | ss.TotalPages = value 315 | case "total_chunks": 316 | ss.TotalChunks = value 317 | case "used_chunks": 318 | ss.UsedChunks = value 319 | case "free_chunks": 320 | ss.FreeChunks = value 321 | case "free_chunks_end": 322 | ss.FreeChunksEnd = value 323 | } 324 | } 325 | if err := scr.Err(); err != nil { 326 | return nil, errors.Wrap(err, "failed to GetSlabStats while scaning stats slabs") 327 | } 328 | 329 | keys := make([]int, 0, len(retMap)) 330 | for i := range retMap { 331 | keys = append(keys, i) 332 | } 333 | sort.Ints(keys) 334 | ret := make([]*SlabStat, len(keys)) 335 | for i, v := range keys { 336 | ret[i] = retMap[v] 337 | } 338 | return ret, nil 339 | } 340 | -------------------------------------------------------------------------------- /memdtool_test.go: -------------------------------------------------------------------------------- 1 | package memdtool 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type mockConn struct { 11 | io.Reader 12 | } 13 | 14 | func (*mockConn) Write(p []byte) (int, error) { 15 | return len(p), nil 16 | } 17 | 18 | func TestGetSlabStats(t *testing.T) { 19 | input := `STAT items:1:number 1 20 | STAT items:1:age 102976348 21 | STAT items:1:evicted 0 22 | STAT items:1:evicted_nonzero 0 23 | STAT items:1:evicted_time 0 24 | STAT items:1:outofmemory 0 25 | STAT items:1:tailrepairs 0 26 | STAT items:1:reclaimed 171237 27 | STAT items:2:number 795 28 | STAT items:2:age 102375571 29 | STAT items:2:evicted 0 30 | STAT items:2:evicted_nonzero 0 31 | STAT items:2:evicted_time 0 32 | STAT items:2:outofmemory 0 33 | STAT items:2:tailrepairs 0 34 | STAT items:2:reclaimed 137342 35 | STAT items:30:number 1 36 | STAT items:30:age 52864995 37 | STAT items:30:evicted 0 38 | STAT items:30:evicted_nonzero 0 39 | STAT items:30:evicted_time 0 40 | STAT items:30:outofmemory 0 41 | STAT items:30:tailrepairs 0 42 | STAT items:30:reclaimed 0 43 | END 44 | STAT 1:chunk_size 96 45 | STAT 1:chunks_per_page 10922 46 | STAT 1:total_pages 1 47 | STAT 1:total_chunks 10922 48 | STAT 1:used_chunks 1 49 | STAT 1:free_chunks 0 50 | STAT 1:free_chunks_end 10921 51 | STAT 1:mem_requested 88 52 | STAT 1:get_hits 171239 53 | STAT 1:cmd_set 171239 54 | STAT 1:delete_hits 0 55 | STAT 1:incr_hits 0 56 | STAT 1:decr_hits 0 57 | STAT 1:cas_hits 0 58 | STAT 1:cas_badval 0 59 | STAT 2:chunk_size 120 60 | STAT 2:chunks_per_page 8738 61 | STAT 2:total_pages 1 62 | STAT 2:total_chunks 8738 63 | STAT 2:used_chunks 795 64 | STAT 2:free_chunks 5710 65 | STAT 2:free_chunks_end 2233 66 | STAT 2:mem_requested 84270 67 | STAT 2:get_hits 18780 68 | STAT 2:cmd_set 143922 69 | STAT 2:delete_hits 0 70 | STAT 2:incr_hits 0 71 | STAT 2:decr_hits 0 72 | STAT 2:cas_hits 0 73 | STAT 2:cas_badval 0 74 | STAT 30:chunk_size 66232 75 | STAT 30:chunks_per_page 15 76 | STAT 30:total_pages 1 77 | STAT 30:total_chunks 15 78 | STAT 30:used_chunks 1 79 | STAT 30:free_chunks 0 80 | STAT 30:free_chunks_end 14 81 | STAT 30:mem_requested 57368 82 | STAT 30:get_hits 0 83 | STAT 30:cmd_set 1 84 | STAT 30:delete_hits 0 85 | STAT 30:incr_hits 0 86 | STAT 30:decr_hits 0 87 | STAT 30:cas_hits 0 88 | STAT 30:cas_badval 0 89 | STAT active_slabs 3 90 | STAT total_malloced 3090552 91 | END 92 | ` 93 | conn := &mockConn{strings.NewReader(input)} 94 | 95 | items, err := GetSlabStats(conn) 96 | if err != nil { 97 | t.Errorf("error should be nil but:%s", err.Error()) 98 | } 99 | 100 | expeced := []*SlabStat{ 101 | { 102 | ID: 1, 103 | Number: 1, 104 | Age: 102976348, 105 | Reclaimed: 171237, 106 | ChunkSize: 96, 107 | ChunksPerPage: 10922, 108 | TotalPages: 1, 109 | TotalChunks: 10922, 110 | UsedChunks: 1, 111 | FreeChunksEnd: 10921}, 112 | { 113 | ID: 2, 114 | Number: 795, 115 | Age: 102375571, 116 | Reclaimed: 137342, 117 | ChunkSize: 120, 118 | ChunksPerPage: 8738, 119 | TotalPages: 1, 120 | TotalChunks: 8738, 121 | UsedChunks: 795, 122 | FreeChunks: 5710, 123 | FreeChunksEnd: 2233}, 124 | { 125 | ID: 30, 126 | Number: 1, 127 | Age: 52864995, 128 | ChunkSize: 0x102b8, 129 | ChunksPerPage: 15, 130 | TotalPages: 1, 131 | TotalChunks: 15, 132 | UsedChunks: 1, 133 | FreeChunksEnd: 14}, 134 | } 135 | if !reflect.DeepEqual(items, expeced) { 136 | t.Errorf("something went wrong") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package memdtool 2 | 3 | const version = "0.1.0" 4 | 5 | var revision = "Devel" 6 | --------------------------------------------------------------------------------