├── .gitignore ├── LICENSE ├── README.md ├── main.go └── redis-monitor-example.log /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryosuke Yabuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rmlp 2 | 3 | rmlp is Redis Monitor Log Profiler. 4 | 5 | Inspired by [facebookarchive/redis-faina](https://github.com/facebookarchive/redis-faina) 6 | 7 | # Installation 8 | 9 | ``` 10 | $ go get github.com/Konboi/rmlp 11 | ``` 12 | 13 | # Useage 14 | 15 | Read from stdin or an input file (-f) 16 | 17 | ``` 18 | redis-cli monitor | head -n 100 | rmlp 19 | 20 | rmlp -f redis-monitor.log 21 | ``` 22 | 23 | ``` 24 | $ rmlp -f redis-monitor-example.log 25 | 26 | Overall Stats 27 | ================================== 28 | LineCount 15000 29 | 30 | 31 | Commands Rate 32 | ================================== 33 | LRANGE 4000 34 | PING 2000 35 | LPUSH 2000 36 | INCR 1000 37 | SADD 1000 38 | SPOP 1000 39 | MSET 1000 40 | LPOP 1000 41 | SET 1000 42 | GET 1000 43 | 44 | Heavy Commands 45 | ================================== 46 | Command Sum(msec) 47 | LRANGE 0.803558 48 | PING 0.106445 49 | LPUSH 0.099761 50 | MSET 0.092236 51 | SET 0.060840 52 | INCR 0.056400 53 | SADD 0.053511 54 | GET 0.051652 55 | LPOP 0.047541 56 | SPOP 0.038231 57 | 58 | Slowest Calls 59 | ================================== 60 | KEY Count Max(msec) Avg(msec) 61 | PING 2000 0.001511 0.000053 62 | LRANGE mylist 4000 0.001358 0.000201 63 | MSET key:__rand_int__ 1000 0.000959 0.000092 64 | LPUSH mylist 2000 0.000856 0.000050 65 | INCR counter:__rand_int__ 1000 0.000837 0.000056 66 | SET key:__rand_int__ 1000 0.000806 0.000061 67 | SPOP myset 1000 0.000705 0.000038 68 | SADD myset 1000 0.000538 0.000054 69 | GET key:__rand_int__ 1000 0.000480 0.000052 70 | LPOP mylist 1000 0.000427 0.000048 71 | 72 | $ rmlp -f redis-monitor-example.log -s avg 73 | Overall Stats 74 | ================================== 75 | LineCount 15000 76 | 77 | 78 | Commands Rate 79 | ================================== 80 | LRANGE 4000 81 | PING 2000 82 | LPUSH 2000 83 | INCR 1000 84 | SADD 1000 85 | SPOP 1000 86 | MSET 1000 87 | LPOP 1000 88 | SET 1000 89 | GET 1000 90 | 91 | Heavy Commands 92 | ================================== 93 | Command Sum(msec) 94 | LRANGE 0.803558 95 | PING 0.106445 96 | LPUSH 0.099761 97 | MSET 0.092236 98 | SET 0.060840 99 | INCR 0.056400 100 | SADD 0.053511 101 | GET 0.051652 102 | LPOP 0.047541 103 | SPOP 0.038231 104 | 105 | Slowest Calls 106 | ================================== 107 | KEY Count Max(msec) Avg(msec) 108 | LRANGE mylist 4000 0.001358 0.000201 109 | MSET key:__rand_int__ 1000 0.000959 0.000092 110 | SET key:__rand_int__ 1000 0.000806 0.000061 111 | INCR counter:__rand_int__ 1000 0.000837 0.000056 112 | SADD myset 1000 0.000538 0.000054 113 | PING 2000 0.001511 0.000053 114 | GET key:__rand_int__ 1000 0.000480 0.000052 115 | LPUSH mylist 2000 0.000856 0.000050 116 | LPOP mylist 1000 0.000427 0.000048 117 | SPOP myset 1000 0.000705 0.000038 118 | ``` 119 | 120 | redis-monitor-example.log created at local. 121 | 122 | ``` 123 | $ redis-benchmark -c 2 -n 1000 124 | ``` 125 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "text/tabwriter" 14 | 15 | "github.com/soh335/sliceflag" 16 | ) 17 | 18 | type Lines []string 19 | 20 | type ByInput struct{ Lines } 21 | 22 | type Line struct { 23 | Epoch float64 24 | DB int 25 | Command string 26 | Key string 27 | Args string 28 | } 29 | 30 | type Command struct { 31 | Name string 32 | Cnt int 33 | Sum float64 34 | Avg float64 35 | } 36 | 37 | type Commands []Command 38 | 39 | type Profile struct { 40 | Call string 41 | Args string 42 | Cnt int 43 | Max float64 44 | Min float64 45 | Avg float64 46 | Sum float64 47 | } 48 | 49 | type Profiles []Profile 50 | 51 | type ByCnt struct{ Profiles } 52 | type ByMax struct{ Profiles } 53 | type ByAvg struct{ Profiles } 54 | 55 | type ByCommand struct{ Commands } 56 | type ByHeavyCommand struct{ Commands } 57 | 58 | func (bi ByInput) Len() int { return len(bi.Lines) } 59 | func (bi ByInput) Swap(i, j int) { 60 | bi.Lines[i], bi.Lines[j] = bi.Lines[j], bi.Lines[i] 61 | } 62 | func (bi ByInput) Less(i, j int) bool { return bi.Lines[i] < bi.Lines[j] } 63 | 64 | func (p Profiles) Len() int { return len(p) } 65 | func (p Profiles) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 66 | 67 | func (c Commands) Len() int { return len(c) } 68 | func (c Commands) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 69 | 70 | func (p ByCnt) Less(i, j int) bool { return p.Profiles[i].Cnt < p.Profiles[j].Cnt } 71 | func (p ByMax) Less(i, j int) bool { return p.Profiles[i].Max < p.Profiles[j].Max } 72 | func (p ByAvg) Less(i, j int) bool { return p.Profiles[i].Avg < p.Profiles[j].Avg } 73 | 74 | func (c ByCommand) Less(i, j int) bool { return c.Commands[i].Cnt < c.Commands[j].Cnt } 75 | func (c ByHeavyCommand) Less(i, j int) bool { return c.Commands[i].Sum < c.Commands[j].Sum } 76 | 77 | const ( 78 | SORT_TYPE_BY_MAX = "max" 79 | SORT_TYPE_BY_AVG = "avg" 80 | SORT_TYPE_BY_CNT = "cnt" 81 | ) 82 | 83 | var ( 84 | filePath = flag.String("f", "", "redis-cli monitor output file") 85 | listNum = flag.Int("n", 10, "Show Slowest Calls Count") 86 | sortType = flag.String("s", "max", "Set SlowestCalls Type: max, avg, cnt") 87 | minCountNum = flag.Int("min", 0, "Show Slowest Calls Count over the minCountNum") 88 | 89 | ignoreStrings = sliceflag.String(flag.CommandLine, "i", []string{}, "Set ignore strings") 90 | 91 | // regexp 92 | // refs: https://play.golang.org/p/yl6B1oWtvE 93 | // 0: line 94 | // 1: epoch 95 | // 2; db 96 | // 3: command 97 | // 4: command args 98 | // 5: key 99 | // 6: args 100 | lineRegexpRule = `([\d\.]+)\s\[(\d+)\s\d+\.\d+\.\d+\.\d+:\d+]\s"(\w+)"(\s"([^(? len(group) { 187 | continue 188 | } 189 | 190 | epoch, err := strconv.ParseFloat(group[1], 64) 191 | if err != nil { 192 | log.Println(err) 193 | continue 194 | } 195 | db, err := strconv.Atoi(group[2]) 196 | if err != nil { 197 | log.Println(err) 198 | continue 199 | } 200 | 201 | line := Line{ 202 | Epoch: epoch, 203 | DB: db, 204 | Command: group[3], 205 | Key: group[5], 206 | Args: group[6], 207 | } 208 | 209 | var commandTime float64 210 | if beforeLine.Command != "" { 211 | commandTime = beforeLine.Epoch - line.Epoch 212 | } else { 213 | commandTime = 0 214 | } 215 | beforeLine = line 216 | 217 | if ignore != "" { 218 | i := ignoreRegexp.FindAllString(input, -1) 219 | if len(i) > 0 { 220 | continue 221 | } 222 | } 223 | 224 | SetProfileIndex(fmt.Sprintf("%s %s", line.Command, line.Key)) 225 | SetCommandIndex(line.Command) 226 | 227 | if monitorLogs[logCursor].Max < commandTime { 228 | monitorLogs[logCursor].Max = commandTime 229 | } 230 | 231 | if commandTime < monitorLogs[logCursor].Min { 232 | monitorLogs[logCursor].Min = commandTime 233 | } 234 | monitorLogs[logCursor].Cnt++ 235 | monitorLogs[logCursor].Sum = monitorLogs[logCursor].Sum + commandTime 236 | monitorLogs[logCursor].Avg = monitorLogs[logCursor].Sum / float64(monitorLogs[logCursor].Cnt) 237 | 238 | commands[commandCursor].Cnt++ 239 | commands[commandCursor].Sum = commands[commandCursor].Sum + commandTime 240 | 241 | lineCount++ 242 | } 243 | 244 | PrintResult() 245 | } 246 | 247 | func PrintTitle(key string) { 248 | fmt.Println(key) 249 | fmt.Println("==================================") 250 | } 251 | 252 | func PrintResult() { 253 | PrintTitle("Overall Stats") 254 | fmt.Printf("LineCount \t %d \n\n", lineCount) 255 | fmt.Printf("\n") 256 | 257 | w := new(tabwriter.Writer) 258 | w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.AlignRight) 259 | PrintTitle("Commands Rate") 260 | sort.Sort(sort.Reverse((ByCommand{commands}))) 261 | for _, v := range commands { 262 | fmt.Fprintf(w, "%s\t %d \n", v.Name, v.Cnt) 263 | } 264 | w.Flush() 265 | fmt.Printf("\n") 266 | 267 | PrintTitle("Heavy Commands") 268 | fmt.Fprintln(w, "Command \tSum(msec)") 269 | sort.Sort(sort.Reverse((ByHeavyCommand{commands}))) 270 | for _, v := range commands { 271 | fmt.Fprintf(w, "%s \t %f \n", v.Name, v.Sum) 272 | } 273 | w.Flush() 274 | fmt.Printf("\n") 275 | 276 | PrintTitle("Slowest Calls") 277 | if strings.Contains(*sortType, SORT_TYPE_BY_AVG) { 278 | sort.Sort(sort.Reverse((ByAvg{monitorLogs}))) 279 | } else if strings.Contains(*sortType, SORT_TYPE_BY_CNT) { 280 | sort.Sort(sort.Reverse((ByCnt{monitorLogs}))) 281 | } else { 282 | sort.Sort(sort.Reverse((ByMax{monitorLogs}))) 283 | } 284 | 285 | fmt.Fprintln(w, "KEY \tCount \tMax(msec) \t Avg(msec)") 286 | count := 0 287 | for _, v := range monitorLogs { 288 | if v.Cnt < *minCountNum { 289 | continue 290 | } 291 | if *listNum < count { 292 | break 293 | } 294 | fmt.Fprintf(w, "%s\t %d \t %f\t %f\n", v.Call, v.Cnt, v.Max, v.Avg) 295 | count++ 296 | } 297 | w.Flush() 298 | } 299 | --------------------------------------------------------------------------------