├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── redis-visualize.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-visualize", 3 | "version": "0.1.1", 4 | "description": "A module to sample redis keys and provide summary data", 5 | "author": "michal.mhr@gmail.com", 6 | "bin": { 7 | "redis-visualize": "./redis-visualize.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mhr3/redis-visualize" 12 | }, 13 | "keywords": [ 14 | "redis", 15 | "scanner", 16 | "sampler", 17 | "analyzer", 18 | "gui", 19 | "visualize" 20 | ], 21 | "dependencies": { 22 | "redis": "2.5.x" 23 | }, 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Xamarin Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom 8 | the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Visualizer 2 | 3 | Redis visualizer is an utility to sample and visualise keys in your Redis instance. 4 | It was insprired by the command line redis-sampler tool (https://github.com/antirez/redis-sampler), 5 | with the goal to provide better user interface. 6 | 7 | Using it is pretty straightforward, install the package globally with: 8 | 9 | `npm install -g redis-visualize` 10 | 11 | And run it using: 12 | 13 | `redis-visualize` 14 | 15 | This will start a webserver on port 8079, and you can connect to it using 16 | your web browser, where you'll be able to specify address, port and password of the actual 17 | Redis host and start sampling. 18 | 19 | Similar keys are automatically clustered using regular expressions run on the key 20 | itself, and you're free to modify the regexes to customize the clustering. 21 | 22 | It is highly recommended to run the utility in the same network as the redis host 23 | and port forward just the HTTP port instead of port forwarding the redis port. This will 24 | achieve much higher sampling throughput. 25 | 26 | ### Example output: 27 | 28 | ![Example output](https://github.com/mhr3/redis-visualize/wiki/images/example-output.png) 29 | -------------------------------------------------------------------------------- /redis-visualize.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) 2016 Xamarin Inc. 4 | * 5 | * Authored by: Michal Hruby 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the "Software"), 9 | * to deal in the Software without restriction, including without limitation 10 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | * and/or sell copies of the Software, and to permit persons to whom 12 | * the Software is furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included 15 | * in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | * IN THE SOFTWARE. 24 | */ 25 | 26 | var http = require('http'); 27 | var fs = require('fs'); 28 | var path = require('path'); 29 | var querystring = require('querystring'); 30 | var url = require('url'); 31 | var util = require('util'); 32 | var redis = require('redis'); 33 | var stream = require('stream'); 34 | var zlib = require('zlib'); 35 | 36 | const PORT = 8079; 37 | 38 | var REDIS_HOST = 'localhost'; 39 | var REDIS_PORT = 6379; 40 | var DEFAULT_TIMEOUT = 500; 41 | var REDIS_PASSWORD = undefined; 42 | 43 | const LUA_SCRIPT = String.raw` 44 | local key = redis.call('randomkey'); 45 | local ttl = redis.call('ttl', key); 46 | local type = redis.call('type', key)['ok']; 47 | 48 | local size = 0; 49 | if type == 'string' then size = redis.call('strlen', key); 50 | elseif type == 'list' then size = redis.call('llen', key); 51 | elseif type == 'set' then size = redis.call('scard', key); 52 | elseif type == 'zset' then size = redis.call('zcard', key); 53 | elseif type == 'hash' then size = redis.call('hlen', key); 54 | end; 55 | 56 | return {key, ttl, type, size} 57 | `.replace(/\n/g, ''); 58 | 59 | var clients = {}; 60 | 61 | function getRedisData(params, done) { 62 | var host = params.redishost || REDIS_HOST; 63 | var port = params.redisport || REDIS_PORT; 64 | var password = params.redispassword || REDIS_PASSWORD; 65 | var timeout = parseInt(params.timeout) || DEFAULT_TIMEOUT; 66 | 67 | var key = util.format("%s:%s", host, port); 68 | var client = clients[key]; 69 | if (!client){ 70 | client = redis.createClient(port, host, { password: password, retry_strategy: (options) => { 71 | if (options.error) { 72 | delete clients[key]; 73 | return options.error; 74 | } 75 | return false; 76 | } }); 77 | // don't want to crash 78 | client.on('error', () => {}); 79 | clients[key] = client; 80 | } 81 | 82 | var results = []; 83 | var startTime = Date.now(); 84 | var getNext = () => { 85 | client.eval(LUA_SCRIPT, 0, (err, keyData) => { 86 | if (err) return done(err); 87 | 88 | results.push({key: keyData[0], ttl: keyData[1], type: keyData[2], size: keyData[3]}); 89 | if (Date.now() - startTime > timeout){ 90 | return done(null, results); 91 | } 92 | getNext(); 93 | }); 94 | }; 95 | 96 | getNext(); 97 | } 98 | 99 | function handleRequest(request, response) { 100 | if (request.method != 'GET'){ 101 | response.writeHead(405, {'Content-Type': 'text/plain'}); 102 | response.end('Request method not supported'); 103 | return; 104 | } 105 | 106 | var parsed = url.parse(request.url); 107 | 108 | switch (parsed.pathname){ 109 | case '/': 110 | case '/index.html': 111 | response.writeHead(200, {'Content-Type': 'text/html'}); 112 | var f = fs.createReadStream(path.join(__dirname, 'index.html')); 113 | f.pipe(response); 114 | break; 115 | case '/api/data': 116 | getRedisData(querystring.parse(parsed.query), (err, data) => { 117 | if (err) { 118 | response.writeHead(500, {'Content-Type': 'application/json'}); 119 | response.end(JSON.stringify({error: err.message})); 120 | return; 121 | } 122 | var content = JSON.stringify(data); 123 | var ae = request.headers['accept-encoding']; 124 | if (ae && ae.match(/\bgzip\b/)){ 125 | response.writeHead(200, {'Content-Type': 'application/json', 'Content-Encoding': 'gzip'}); 126 | var r = new stream.Readable(); 127 | r.push(content); 128 | r.push(null); 129 | r.pipe(zlib.createGzip()).pipe(response); 130 | } else { 131 | response.writeHead(200, {'Content-Type': 'application/json'}); 132 | response.end(content); 133 | } 134 | }); 135 | break; 136 | default: 137 | response.writeHead(404, {'Content-Type': 'text/plain'}); 138 | response.end('Not found'); 139 | break; 140 | } 141 | } 142 | 143 | var argv = process.argv; 144 | var port = PORT; 145 | var server = http.createServer(handleRequest); 146 | 147 | if (argv.indexOf('-h') !== -1 || argv.indexOf('--help') !== -1){ 148 | console.log('usage: redis-visualize -p [PORT]'); 149 | return; 150 | } 151 | 152 | var index; 153 | if ((index = argv.indexOf('-p')) !== -1 || (index = argv.indexOf('--port')) !== -1){ 154 | port = parseInt(argv[index+1]) || PORT; 155 | } 156 | 157 | console.log('Listening on ' + port + '...'); 158 | server.listen(port); 159 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 57 | 58 | 481 | 482 | 483 |
484 | Host: 485 |
486 | 487 |
488 | 489 |
    490 | Port: 491 |
    492 | Password: 493 |
    494 | 495 | 496 | 497 | 498 |
499 |
500 |
501 | 519 | 520 | 521 | 522 | 528 | 531 | 532 | --------------------------------------------------------------------------------