├── .jshintrc ├── LICENSE ├── README.md ├── ansi.js ├── package.json ├── queue.js ├── redis_parser.js └── redis_trace /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": false, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": "nofunc", 10 | "newcap": false, 11 | "noarg": true, 12 | "nonew": true, 13 | "plusplus": false, 14 | "quotmark": "double", 15 | "regexp": false, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "noempty": true, 21 | "globals": { 22 | "console": true, 23 | "Buffer": true, 24 | "setTimeout": true, 25 | "clearTimeout": true, 26 | "setInterval": true, 27 | "clearInterval": true, 28 | "require": false, 29 | "module": false, 30 | "exports": true, 31 | "global": false, 32 | "process": true, 33 | "__dirname": false, 34 | "__filename": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matt Ranney 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis_trace 2 | 3 | Decode redis protocol from packet capture to better understand who is using your redis server and what they are doing with it. 4 | 5 | Install with: `npm -g install redis_trace` 6 | 7 | ## Example 8 | 9 | ``` 10 | $ sudo ./redis_trace -f "port 6379" -i lo0 11 | Listening on lo0 12 | 03:12:36.562 127.0.0.1:61318 -> 127.0.0.1:6379 TCP start 13 | 03:12:36.568 127.0.0.1:6379 -> 127.0.0.1:61318 1/1 get [foo] -> bar 0.092ms 14 | 03:12:36.572 127.0.0.1:6379 -> 127.0.0.1:61318 2/2 keys [*] -> [a key, foo_rand000000000000, foo, foo2] 0.104ms 15 | 03:12:36.579 127.0.0.1:6379 -> 127.0.0.1:61318 3/3 get [foo2] -> bar2 0.087ms 16 | 03:12:36.593 127.0.0.1:61318 -> 127.0.0.1:6379 TCP end 31.564s 3 commands 0.10 req/sec 17 | ``` 18 | 19 | This differs from running the redis `MONITOR` command because it does not connect to a redis-server process. 20 | Instead, it watches the packets go by from the network. This allows us to do some pretty interesting things, 21 | such as break down traffic by client, by key, etc. Also, because we are seeing traffic in both directions, 22 | we can analyze the responses to each command, whereas `MONITOR` only shows the command itself. 23 | 24 | This program may end up being useful as is, but I hope you'll find it useful to do other types of analysis of 25 | the redis protocol. The protocol is decoded as a JavaScript object from the link layer frame all the way up to 26 | redis commands and replies. 27 | 28 | For example, you can use the `--tcp-verbose` flag to analyze the TCP performance of each connection you see go by: 29 | 30 | ``` 31 | $ sudo ./redis_trace -f "port 6379" -i lo0 --tcp-verbose 32 | Password: 33 | Listening on lo0 34 | 03:12:37.295 127.0.0.1:61421 -> 127.0.0.1:6379 TCP start 35 | 03:12:37.295 127.0.0.1:6379 -> 127.0.0.1:61421 1/1 mget [foo, foo2] -> [bar, bar2] 0.081ms 36 | 03:12:37.295 127.0.0.1:61421 -> 127.0.0.1:6379 TCP end 0.001s 1 commands 1000.00 req/sec 37 | Set stats for session: { recv_times: 38 | { '2211033425': 0.00006985664367675781, 39 | '2211033448': 0.00023102760314941406 }, 40 | send_times: { '2244795084': 0.00008106231689453125 }, 41 | send_retrans: {}, 42 | recv_retrans: {}, 43 | connect_duration: 0.00008082389831542969, 44 | total_time: 0.0005328655242919922, 45 | send_overhead: 272, 46 | send_payload: 33, 47 | send_total: 305, 48 | recv_overhead: 220, 49 | recv_payload: 23, 50 | recv_total: 243 } 51 | ``` 52 | -------------------------------------------------------------------------------- /ansi.js: -------------------------------------------------------------------------------- 1 | // http://en.wikipedia.org/wiki/ANSI_escape_code 2 | var formats = { 3 | bold: [1, 22], // bright 4 | light: [2, 22], // faint 5 | italic: [3, 23], 6 | underline: [4, 24], // underline single 7 | blink_slow: [5, 25], 8 | blink_fast: [6, 25], 9 | inverse: [7, 27], 10 | conceal: [8, 28], 11 | strikethrough: [9, 29], // crossed-out 12 | // 10 - 20 are font control 13 | underline_double: [21, 24], 14 | black: [30, 39], 15 | red: [31, 39], 16 | green: [32, 39], 17 | yellow: [33, 39], 18 | blue: [34, 39], 19 | magenta: [35, 39], 20 | cyan: [36, 39], 21 | white: [37, 39], 22 | grey: [90, 39] 23 | }; 24 | 25 | var CSI = String.fromCharCode(27) + "["; 26 | 27 | Object.keys(formats).forEach(function (format) { 28 | exports[format] = function fmt(str) { 29 | if (exports.no_color) { 30 | return str; 31 | } 32 | return CSI + formats[format][0] + "m" + str + CSI + formats[format][1] + "m"; 33 | }; 34 | }); 35 | 36 | exports.no_color = false; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "redis_trace", 2 | "version" : "1.0.0", 3 | "description" : "Live Redis packet capture and protocol decoding", 4 | "author": "Matt Ranney ", 5 | "main": "./redis_trace", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/mranney/redis_trace.git" 9 | }, 10 | "dependencies": { 11 | "pcap": ">=2.0.0" 12 | }, 13 | "bin": { 14 | "redis_trace": "./redis_trace" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /queue.js: -------------------------------------------------------------------------------- 1 | // Queue class adapted from Tim Caswell's pattern library 2 | // http://github.com/creationix/pattern/blob/master/lib/pattern/queue.js 3 | 4 | function Queue() { 5 | this.tail = []; 6 | this.head = []; 7 | this.offset = 0; 8 | } 9 | 10 | Queue.prototype.shift = function () { 11 | if (this.offset === this.head.length) { 12 | var tmp = this.head; 13 | tmp.length = 0; 14 | this.head = this.tail; 15 | this.tail = tmp; 16 | this.offset = 0; 17 | if (this.head.length === 0) { 18 | return; 19 | } 20 | } 21 | return this.head[this.offset++]; // sorry, JSLint 22 | }; 23 | 24 | Queue.prototype.push = function (item) { 25 | return this.tail.push(item); 26 | }; 27 | 28 | Queue.prototype.forEach = function (fn, thisv) { 29 | var array = this.head.slice(this.offset), i, il; 30 | 31 | array.push.apply(array, this.tail); 32 | 33 | if (thisv) { 34 | for (i = 0, il = array.length; i < il; i += 1) { 35 | fn.call(thisv, array[i], i, array); 36 | } 37 | } else { 38 | for (i = 0, il = array.length; i < il; i += 1) { 39 | fn(array[i], i, array); 40 | } 41 | } 42 | 43 | return array; 44 | }; 45 | 46 | Queue.prototype.getLength = function () { 47 | return this.head.length - this.offset + this.tail.length; 48 | }; 49 | 50 | Object.defineProperty(Queue.prototype, "length", { 51 | get: function () { 52 | return this.getLength(); 53 | } 54 | }); 55 | 56 | 57 | if (typeof module !== "undefined" && module.exports) { 58 | module.exports = Queue; 59 | } 60 | -------------------------------------------------------------------------------- /redis_parser.js: -------------------------------------------------------------------------------- 1 | var events = require("events"); 2 | var util = require("util"); 3 | 4 | function Packet(type, size) { 5 | this.type = type; 6 | this.size = +size; 7 | } 8 | 9 | exports.name = "javascript"; 10 | exports.debug_mode = false; 11 | 12 | function ReplyParser(options) { 13 | this.name = exports.name; 14 | this.options = options || { }; 15 | 16 | this._buffer = null; 17 | this._offset = 0; 18 | this._encoding = "utf-8"; 19 | this._debug_mode = options.debug_mode; 20 | this._reply_type = null; 21 | } 22 | 23 | util.inherits(ReplyParser, events.EventEmitter); 24 | 25 | exports.ReplyParser = ReplyParser; 26 | 27 | function IncompleteReadBuffer(message) { 28 | this.name = "IncompleteReadBuffer"; 29 | this.message = message; 30 | } 31 | util.inherits(IncompleteReadBuffer, Error); 32 | 33 | // Buffer.toString() is quite slow for small strings 34 | function small_toString(buf, start, end) { 35 | var tmp = "", i; 36 | 37 | for (i = start; i < end; i++) { 38 | tmp += String.fromCharCode(buf[i]); 39 | } 40 | 41 | return tmp; 42 | } 43 | 44 | ReplyParser.prototype._parseResult = function (type) { 45 | var start, end, offset, packetHeader; 46 | 47 | if (type === 43 || type === 45) { // + or - 48 | // up to the delimiter 49 | end = this._packetEndOffset() - 1; 50 | start = this._offset; 51 | 52 | // include the delimiter 53 | this._offset = end + 2; 54 | 55 | if (end > this._buffer.length) { 56 | this._offset = start; 57 | throw new IncompleteReadBuffer("Wait for more data."); 58 | } 59 | 60 | if (this.options.return_buffers) { 61 | return this._buffer.slice(start, end); 62 | } else { 63 | if (end - start < 65536) { // completely arbitrary 64 | return small_toString(this._buffer, start, end); 65 | } else { 66 | return this._buffer.toString(this._encoding, start, end); 67 | } 68 | } 69 | } else if (type === 58) { // : 70 | // up to the delimiter 71 | end = this._packetEndOffset() - 1; 72 | start = this._offset; 73 | 74 | // include the delimiter 75 | this._offset = end + 2; 76 | 77 | if (end > this._buffer.length) { 78 | this._offset = start; 79 | throw new IncompleteReadBuffer("Wait for more data."); 80 | } 81 | 82 | if (this.options.return_buffers) { 83 | return this._buffer.slice(start, end); 84 | } 85 | 86 | // return the coerced numeric value 87 | return +small_toString(this._buffer, start, end); 88 | } else if (type === 36) { // $ 89 | // set a rewind point, as the packet could be larger than the 90 | // buffer in memory 91 | offset = this._offset - 1; 92 | 93 | packetHeader = new Packet(type, this.parseHeader()); 94 | 95 | // packets with a size of -1 are considered null 96 | if (packetHeader.size === -1) { 97 | return undefined; 98 | } 99 | 100 | end = this._offset + packetHeader.size; 101 | start = this._offset; 102 | 103 | // set the offset to after the delimiter 104 | this._offset = end + 2; 105 | 106 | if (end > this._buffer.length) { 107 | this._offset = offset; 108 | throw new IncompleteReadBuffer("Wait for more data."); 109 | } 110 | 111 | if (this.options.return_buffers) { 112 | return this._buffer.slice(start, end); 113 | } else { 114 | return this._buffer.toString(this._encoding, start, end); 115 | } 116 | } else if (type === 42) { // * 117 | offset = this._offset; 118 | packetHeader = new Packet(type, this.parseHeader()); 119 | 120 | if (packetHeader.size < 0) { 121 | return null; 122 | } 123 | 124 | if (packetHeader.size > this._bytesRemaining()) { 125 | this._offset = offset - 1; 126 | throw new IncompleteReadBuffer("Wait for more data."); 127 | } 128 | 129 | var reply = [ ]; 130 | var ntype, i, res; 131 | 132 | offset = this._offset - 1; 133 | 134 | for (i = 0; i < packetHeader.size; i++) { 135 | ntype = this._buffer[this._offset++]; 136 | 137 | if (this._offset > this._buffer.length) { 138 | throw new IncompleteReadBuffer("Wait for more data."); 139 | } 140 | res = this._parseResult(ntype); 141 | if (res === undefined) { 142 | res = null; 143 | } 144 | reply.push(res); 145 | } 146 | 147 | return reply; 148 | } 149 | }; 150 | 151 | ReplyParser.prototype.execute = function (buffer) { 152 | this.append(buffer); 153 | 154 | var type, ret, offset; 155 | 156 | while (true) { 157 | offset = this._offset; 158 | try { 159 | // at least 4 bytes: :1\r\n 160 | if (this._bytesRemaining() < 4) { 161 | break; 162 | } 163 | 164 | type = this._buffer[this._offset++]; 165 | 166 | if (type === 43) { // + 167 | ret = this._parseResult(type); 168 | 169 | if (ret === null) { 170 | break; 171 | } 172 | 173 | this.send_reply(ret); 174 | } else if (type === 45) { // - 175 | ret = this._parseResult(type); 176 | 177 | if (ret === null) { 178 | break; 179 | } 180 | 181 | this.send_error(ret); 182 | } else if (type === 58) { // : 183 | ret = this._parseResult(type); 184 | 185 | if (ret === null) { 186 | break; 187 | } 188 | 189 | this.send_reply(ret); 190 | } else if (type === 36) { // $ 191 | ret = this._parseResult(type); 192 | 193 | if (ret === null) { 194 | break; 195 | } 196 | 197 | // check the state for what is the result of 198 | // a -1, set it back up for a null reply 199 | if (ret === undefined) { 200 | ret = null; 201 | } 202 | 203 | this.send_reply(ret); 204 | } else if (type === 42) { // * 205 | // set a rewind point. if a failure occurs, 206 | // wait for the next execute()/append() and try again 207 | offset = this._offset - 1; 208 | 209 | ret = this._parseResult(type); 210 | 211 | this.send_reply(ret); 212 | } 213 | } catch (err) { 214 | // catch the error (not enough data), rewind, and wait 215 | // for the next packet to appear 216 | if (! (err instanceof IncompleteReadBuffer)) { 217 | throw err; 218 | } 219 | this._offset = offset; 220 | break; 221 | } 222 | } 223 | }; 224 | 225 | ReplyParser.prototype.append = function (newBuffer) { 226 | if (!newBuffer) { 227 | return; 228 | } 229 | 230 | // first run 231 | if (this._buffer === null) { 232 | this._buffer = newBuffer; 233 | 234 | return; 235 | } 236 | 237 | // out of data 238 | if (this._offset >= this._buffer.length) { 239 | this._buffer = newBuffer; 240 | this._offset = 0; 241 | 242 | return; 243 | } 244 | 245 | // very large packet 246 | // check for concat, if we have it, use it 247 | if (Buffer.concat !== undefined) { 248 | this._buffer = Buffer.concat([this._buffer.slice(this._offset), newBuffer]); 249 | } else { 250 | var remaining = this._bytesRemaining(), 251 | newLength = remaining + newBuffer.length, 252 | tmpBuffer = new Buffer(newLength); 253 | 254 | this._buffer.copy(tmpBuffer, 0, this._offset); 255 | newBuffer.copy(tmpBuffer, remaining, 0); 256 | 257 | this._buffer = tmpBuffer; 258 | } 259 | 260 | this._offset = 0; 261 | }; 262 | 263 | ReplyParser.prototype.parseHeader = function () { 264 | var end = this._packetEndOffset(), 265 | value = small_toString(this._buffer, this._offset, end - 1); 266 | 267 | this._offset = end + 1; 268 | 269 | return value; 270 | }; 271 | 272 | ReplyParser.prototype._packetEndOffset = function () { 273 | var offset = this._offset; 274 | 275 | while (this._buffer[offset] !== 0x0d && this._buffer[offset + 1] !== 0x0a) { 276 | offset++; 277 | 278 | if (offset >= this._buffer.length) { 279 | throw new IncompleteReadBuffer("didn't see LF after NL reading multi bulk count (" + offset + " => " + this._buffer.length + ", " + this._offset + ")"); 280 | } 281 | } 282 | 283 | offset++; 284 | return offset; 285 | }; 286 | 287 | ReplyParser.prototype._bytesRemaining = function () { 288 | return (this._buffer.length - this._offset) < 0 ? 0 : (this._buffer.length - this._offset); 289 | }; 290 | 291 | ReplyParser.prototype.parser_error = function (message) { 292 | this.emit("error", message); 293 | }; 294 | 295 | ReplyParser.prototype.send_error = function (reply) { 296 | this.emit("reply error", reply); 297 | }; 298 | 299 | ReplyParser.prototype.send_reply = function (reply) { 300 | this.emit("reply", reply); 301 | }; 302 | -------------------------------------------------------------------------------- /redis_trace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var inspect = require("util").inspect; 4 | var pcap = require("pcap"); 5 | var pcap_session, ANSI, options = {}; 6 | var ANSI = require("./ansi"); 7 | var ReplyParser = require("./redis_parser").ReplyParser; 8 | var Queue = require("./queue"); 9 | 10 | function lpad(num, len) { 11 | var str = num.toString(); 12 | 13 | while (str.length < len) { 14 | str = "0" + str; 15 | } 16 | return str; 17 | } 18 | 19 | function format_timestamp(timems) { 20 | var date_obj = new Date(timems); 21 | 22 | return ANSI.blue(lpad(date_obj.getHours(), 2) + ":" + lpad(date_obj.getMinutes(), 2) + ":" + lpad(date_obj.getSeconds(), 2) + "." + 23 | lpad(date_obj.getMilliseconds(), 3)); 24 | } 25 | 26 | function format_hostname(hostname) { 27 | if (/[a-zA-Z]/.test(hostname)) { 28 | var parts = hostname.split(":"); 29 | return ANSI.magenta(parts[0].slice(0, 20) + ":" + parts[1]); 30 | } else { 31 | return ANSI.magenta(hostname); 32 | } 33 | } 34 | 35 | function format_line_start(session, send) { 36 | if (send) { 37 | return format_timestamp(session.current_cap_time) + " " + format_hostname(session.src_name) + " -> " + format_hostname(session.dst_name); 38 | } 39 | return format_timestamp(session.current_cap_time) + " " + format_hostname(session.dst_name) + " -> " + format_hostname(session.src_name); 40 | } 41 | 42 | function usage_die(message) { 43 | if (message) { 44 | console.error(""); 45 | console.error(message); 46 | } 47 | console.error(""); 48 | console.error("usage: redis_trace [options]"); 49 | console.error(""); 50 | console.error("Capture options:"); 51 | console.error(" -i interface name for capture (def: first with an addr)"); 52 | console.error(" -f packet filter in pcap-filter(7) syntax (def: all TCP packets)"); 53 | console.error(" -b size in MB to buffer between libpcap and app (def: 10)"); 54 | console.error(""); 55 | process.exit(1); 56 | } 57 | 58 | function parse_options() { 59 | var argv_slice = process.argv.slice(2), 60 | optnum = 0, opt, optname, 61 | state = "match optname", matches, 62 | valid_options; 63 | 64 | valid_options = { 65 | "i": { multiple: false, has_value: true }, 66 | "f": { multiple: false, has_value: true }, 67 | "b": { multiple: false, has_value: true }, 68 | "tcp-verbose": { multiple: false, has_value: false }, 69 | "no-color": { multiple: false, has_value: false }, 70 | "help": { multiple: false, has_value: false } 71 | }; 72 | 73 | function set_option(name, value) { 74 | if (valid_options[name].multiple) { 75 | if (valid_options[name].regex) { 76 | value = new RegExp(value); 77 | } 78 | if (options[name] === undefined) { 79 | options[name] = [value]; 80 | } else { 81 | options[name].push(value); 82 | } 83 | } else { 84 | if (options[name] === undefined) { 85 | options[name] = value; 86 | } else { 87 | usage_die("Option " + name + " may only be specified once."); 88 | } 89 | } 90 | } 91 | 92 | while (optnum < argv_slice.length) { 93 | opt = argv_slice[optnum]; 94 | 95 | if (state === "match optname") { 96 | matches = opt.match(/^[\-]{1,2}([^\-].*)/); 97 | if (matches !== null) { 98 | optname = matches[1]; 99 | if (valid_options[optname]) { // if this is a known option 100 | if (valid_options[optname].has_value) { 101 | state = "match optval"; 102 | } else { 103 | set_option(optname, true); 104 | } 105 | } else { 106 | usage_die("Invalid option name: " + optname); 107 | } 108 | } else { 109 | usage_die("bad option name: " + opt); 110 | } 111 | } else if (state === "match optval") { 112 | if (opt[0] !== "-") { 113 | set_option(optname, opt); 114 | state = "match optname"; 115 | } else { 116 | usage_die("bad option value: " + opt); 117 | } 118 | } else { 119 | throw new Error("Unknown state " + state + " in options parser"); 120 | } 121 | 122 | optnum += 1; 123 | } 124 | if (state === "match optval") { 125 | usage_die("Missing option value for " + optname); 126 | } 127 | } 128 | 129 | function privs_check() { 130 | if (process.getuid() !== 0) { 131 | console.log(ANSI.bold(ANSI.red("Warning: not running with root privs, which are usually required for raw packet capture."))); 132 | console.log(ANSI.red("Trying to open anyway...")); 133 | } 134 | } 135 | 136 | function start_capture_session() { 137 | if (! options.f) { 138 | // default filter is all IPv4 TCP, which is all we know how to decode right now anyway 139 | options.f = "ip proto \\tcp and port 6379"; 140 | } 141 | pcap_session = pcap.createSession(options.i, options.f, (options.b * 1024 * 1024)); 142 | console.log("Listening on " + pcap_session.device_name); 143 | } 144 | 145 | function start_drop_watcher() { 146 | // Check for pcap dropped packets on an interval 147 | var first_drop = setInterval(function () { 148 | var stats = pcap_session.stats(); 149 | if (stats.ps_drop > 0) { 150 | console.log(ANSI.bold("pcap dropped packets, need larger buffer or less work to do: " + JSON.stringify(stats))); 151 | clearInterval(first_drop); 152 | setInterval(function () { 153 | console.log(ANSI.bold("pcap dropped packets: " + JSON.stringify(stats))); 154 | }, 5000); 155 | } 156 | }, 1000); 157 | } 158 | 159 | function setup_listeners() { 160 | var tcp_tracker = new pcap.TCPTracker(); 161 | 162 | pcap_session.on("packet", function (raw_packet) { 163 | var packet = pcap.decode.packet(raw_packet); 164 | tcp_tracker.track_packet(packet); 165 | }); 166 | 167 | // tracker emits sessions, and sessions emit data 168 | tcp_tracker.on("session", function (tcp_session) { 169 | on_tcp_session(tcp_session); 170 | }); 171 | } 172 | 173 | function format_req(req) { 174 | var ret = ANSI.green(req[0]); 175 | if (req.length > 1) { 176 | ret += " [" + req.slice(1).join(", ") + "]"; 177 | } 178 | return ret; 179 | } 180 | 181 | function format_res(res) { 182 | if (Array.isArray(res)) { 183 | return ANSI.cyan("[" + res.join(", ") + "]"); 184 | } 185 | if (typeof res === "string") { 186 | return ANSI.cyan(res); 187 | } 188 | return ANSI.cyan(inspect(res)); 189 | } 190 | 191 | function format_sub(res) { 192 | return ANSI.green(res[0]) + " " + ANSI.cyan("[" + res.slice(1).join(", ") + "]"); 193 | } 194 | 195 | var total_ops = 0; 196 | 197 | function RedisReq(req, start) { 198 | this.req = req; 199 | this.start = start; 200 | } 201 | 202 | function on_tcp_session(tcp_session) { 203 | var req_parser = new ReplyParser({}); 204 | var res_parser = new ReplyParser({}); 205 | var command_queue = new Queue(); 206 | var session_start = tcp_session.current_cap_time; 207 | var command_count = 0; 208 | 209 | if (tcp_session.missed_syn) { 210 | console.log(format_line_start(tcp_session, true) + " TCP already in progress "); 211 | } else { 212 | console.log(format_line_start(tcp_session, true) + " TCP start "); 213 | } 214 | 215 | tcp_session.on("data send", function (session, chunk) { 216 | req_parser.execute(chunk); 217 | }); 218 | tcp_session.on("data recv", function (session, chunk) { 219 | res_parser.execute(chunk); 220 | }); 221 | tcp_session.on("end", function (session) { 222 | var dur = (session.current_cap_time - session_start).toFixed(3); 223 | console.log(format_line_start(session, true) + " TCP end " + dur + "s " + command_count + " commands " + 224 | (command_count/dur).toFixed(2) + " req/sec"); 225 | }); 226 | 227 | req_parser.on("reply", function (req) { 228 | command_queue.push(new RedisReq(req, tcp_session.current_cap_time)); 229 | command_count++; 230 | total_ops++; 231 | }); 232 | req_parser.on("error", function (err) { 233 | console.log("Got req error: ", err); 234 | }); 235 | 236 | // Change this function to do other types of analysis 237 | res_parser.on("reply", function (reply) { 238 | var req = command_queue.shift(); 239 | if (!req) { 240 | if (Array.isArray(reply) && (reply[0] === "message" || reply[0] === "pmessage")) { 241 | console.log(format_line_start(tcp_session) + " " + format_sub(reply)); 242 | } else { 243 | console.log(format_line_start(tcp_session) + " reply with no req: " + format_res(reply)); 244 | } 245 | return; 246 | } 247 | var duration = ((tcp_session.current_cap_time - req.start) * 1000).toFixed(3); 248 | console.log(format_line_start(tcp_session) + " " + command_count + "/" + total_ops + " " + format_req(req.req) + " -> " + 249 | format_res(reply) + " " + duration + "ms"); 250 | }); 251 | 252 | res_parser.on("error", function (err) { 253 | console.log("Got reply error: ", err); 254 | }); 255 | 256 | if (options["tcp-verbose"]) { 257 | tcp_session.on("retransmit", function (session, direction, seqno) { 258 | console.log(format_line_start(session, direction === "send") + " TCP retransmit at " + seqno); 259 | }); 260 | tcp_session.on("reset", function (session) { 261 | // eventually this event will have a direction. Right now, it's only from dst. 262 | console.log(format_line_start(session, false) + " TCP reset "); 263 | }); 264 | tcp_session.on("syn retry", function (session) { 265 | console.log(format_line_start(session, true) + " SYN retry"); 266 | }); 267 | tcp_session.on("end", function (session) { 268 | console.log("Set stats for session: ", session.session_stats()); 269 | }); 270 | } 271 | } 272 | 273 | // Make it all go 274 | parse_options(); 275 | if (options["no-color"]) { 276 | ANSI.no_color = true; 277 | } 278 | if (options.help) { 279 | usage_die(); 280 | } 281 | privs_check(); 282 | start_capture_session(); 283 | start_drop_watcher(); 284 | setup_listeners(); 285 | --------------------------------------------------------------------------------