├── public ├── favicon.png ├── redis-logo.png ├── main.css └── main.js ├── screenshot ├── rsm-cmd.png ├── rsm-main-1.png ├── rsm-main-2.png ├── rsm-stat-1.png ├── rsm-stat-2.png └── rsm-stat-3.png ├── views ├── footer.jade ├── header.jade ├── cmd.jade ├── layout.jade ├── stat.jade └── home.jade ├── .gitignore ├── utils ├── logger.js ├── resp.js ├── cmdRespParser.js ├── template.js ├── time.js └── staticServ.js ├── controllers ├── index.js ├── cmd_page.js ├── stat_page.js ├── cmd.js ├── home.js └── stat.js ├── monitor ├── monitor.js └── sentinel.js ├── god.js ├── README.md ├── package.json ├── gulpfile.js ├── config.js ├── app.js ├── init.js ├── models └── db.js └── routes.js /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/redis-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/public/redis-logo.png -------------------------------------------------------------------------------- /screenshot/rsm-cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-cmd.png -------------------------------------------------------------------------------- /screenshot/rsm-main-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-main-1.png -------------------------------------------------------------------------------- /screenshot/rsm-main-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-main-2.png -------------------------------------------------------------------------------- /screenshot/rsm-stat-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-stat-1.png -------------------------------------------------------------------------------- /screenshot/rsm-stat-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-stat-2.png -------------------------------------------------------------------------------- /screenshot/rsm-stat-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitelife/redis-sentinel-ui/HEAD/screenshot/rsm-stat-3.png -------------------------------------------------------------------------------- /views/footer.jade: -------------------------------------------------------------------------------- 1 | //- 2 | @file: footer 3 | @author: gejiawen 4 | @date: 15/12/5 19:15 5 | @description: footer 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | public/dist/ 4 | public/vendor/ 5 | .idea/ 6 | node_modules/ 7 | rsm.db 8 | npm-debug.log 9 | .vscode/ -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Log = require('log'); 4 | 5 | let config = require('../config'); 6 | 7 | let _logger = new Log(config.log_level ? config.log_level : 'info'); 8 | 9 | module.exports = _logger; -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: index 3 | * @author: gejiawen 4 | * @date: 15/12/5 16:52 5 | * @description: index 6 | */ 7 | 8 | 'use strict'; 9 | 10 | exports.Home = require('./home'); 11 | exports.Cmd = require('./cmd'); 12 | exports.Stat = require('./stat'); 13 | exports.Cmd_page = require('./cmd_page'); 14 | exports.Stat_page = require('./stat_page'); 15 | -------------------------------------------------------------------------------- /controllers/cmd_page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let ValidRedisCMDs = require('ioredis/commands'); 4 | 5 | let Template = require('../utils/template'); 6 | 7 | function _cmd_page(req, res) { 8 | res.write(Template.render('views/cmd.jade', { 9 | cmdList: Object.getOwnPropertyNames(ValidRedisCMDs) 10 | })); 11 | res.end(); 12 | } 13 | 14 | module.exports = _cmd_page; 15 | -------------------------------------------------------------------------------- /utils/resp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiayf on 15/12/6. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | function _resp(content, code) { 8 | if (code === undefined) { 9 | code = 200; 10 | } 11 | this.statusCode = code; 12 | if (code >= 400) { 13 | // 解决响应体乱码问题 14 | this.setHeader('Content-Type', 'text/plain; charset=utf-8'); 15 | } 16 | this.write(content); 17 | this.end(); 18 | } 19 | 20 | exports.toRespone = _resp; -------------------------------------------------------------------------------- /utils/cmdRespParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiayf on 15/12/20. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | function _infoRespParser(resp) { 8 | let mapper = {}; 9 | 10 | resp.forEach(val => { 11 | if (val.indexOf(':') === -1) { 12 | return; 13 | } 14 | 15 | let pairs = val.split(':'); 16 | mapper[pairs[0]] = pairs[1]; 17 | }); 18 | 19 | return mapper; 20 | } 21 | 22 | exports.infoRespParser = _infoRespParser; -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | margin-top: 20px; 3 | } 4 | 5 | .navbar-header { 6 | margin-top: 5px; 7 | color: grey; 8 | font-size: 20px; 9 | } 10 | 11 | .navbar-header > img { 12 | display: inline-block; 13 | } 14 | 15 | .navbar-header > span { 16 | margin-left: 10px; 17 | } 18 | 19 | h3 { 20 | text-align: center; 21 | } 22 | 23 | .form-horizontal > button { 24 | float: right; 25 | } 26 | 27 | .error-tip, .loading-tip { 28 | margin-top: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /views/header.jade: -------------------------------------------------------------------------------- 1 | //- 2 | @file: header 3 | @author: gejiawen 4 | @date: 15/12/5 19:15 5 | @description: header 6 | 7 | nav(class=["navbar", "navbar-inverse"]) 8 | div(class="container-fluid") 9 | div(class="navbar-header") 10 | img(alt="logo", src="/public/redis-logo.png", width="50px") 11 | span RSM 12 | ul(class=["nav", "navbar-nav"]) 13 | li 14 | a(href="/") 概况 15 | li 16 | a(href="/stat_page") 监控数据图 17 | li 18 | a(href="/cmd_page") 执行命令 19 | -------------------------------------------------------------------------------- /views/cmd.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h3 执行命令 5 | div(class="container-fluid") 6 | form(class="form-horizontal") 7 | div(class="form-group") 8 | input(type="text", class="form-control", placeholder="输入命令,参数以空格相隔", name="cmd", list="cmd_list") 9 | datalist(id="cmd_list") 10 | each cmd in cmdList 11 | option(value="#{cmd}") 12 | button(type="submit", class=["btn", "btn-primary"], id="submit_cmd") 执行 13 | 14 | div(class="clearfix") 15 | 16 | h3 输出 17 | div(id="cmd_output") 18 | -------------------------------------------------------------------------------- /monitor/monitor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiayf on 15/12/24. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | let RedisSentinel = require('./sentinel'); 8 | let config = require('../config'); 9 | 10 | // 先获取一下 11 | setTimeout(RedisSentinel.update_sentinel_status, 3000); 12 | 13 | // 先执行一下,但得先等RedisSentinel.update_sentinel_status()执行完才能执行 14 | setTimeout(RedisSentinel.fetch_cluster_status, 5000); 15 | 16 | setInterval(RedisSentinel.update_sentinel_status, config.sentinel_status_interval); 17 | setInterval(RedisSentinel.fetch_cluster_status, config.cluster_info_interval); 18 | setInterval(RedisSentinel.collect_server_info, config.server_stat); -------------------------------------------------------------------------------- /god.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | let child_process = require('child_process'); 6 | 7 | let monitor = child_process.fork('./monitor/monitor.js'); 8 | 9 | let exitCallback = function (code, signal) { 10 | console.log('monitor.js, code: ' + code + ', signal: ' + signal); 11 | if (signal === 'SIGTERM') { 12 | return; 13 | } 14 | monitor = child_process.fork('./monitor/monitor.js'); 15 | monitor.on('exit', exitCallback); 16 | }; 17 | 18 | monitor.on('exit', exitCallback); 19 | 20 | process.on('SIGTERM', function () { 21 | console.log('god.js, SIGTERM...'); 22 | monitor.kill(); 23 | }); -------------------------------------------------------------------------------- /utils/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: template 3 | * @author: gejiawen 4 | * @date: 15/12/5 18:02 5 | * @description: template 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const jade = require('jade'); 12 | 13 | let templateCache = {}; 14 | 15 | /** 16 | * 17 | * @param relativePath 18 | * @param data 19 | * @returns {*} 20 | * @private 21 | */ 22 | function _render(relativePath, data) { 23 | // templateCache = {}; 24 | let filePath = path.join(global.RootDir, relativePath); 25 | 26 | if (!(filePath in templateCache)) { 27 | templateCache[filePath] = jade.compileFile(filePath, { 28 | pretty: true 29 | }); 30 | } 31 | 32 | return templateCache[filePath](data); 33 | } 34 | 35 | /** 36 | * Module Exports 37 | */ 38 | exports.render = _render; 39 | -------------------------------------------------------------------------------- /utils/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: time 3 | * @author: gejiawen 4 | * @date: 15/12/5 18:13 5 | * @description: time 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const StdUtil = require('util'); 11 | 12 | // const moment = require('moment'); 13 | 14 | function _formatUpTime(seconds) { 15 | let day = Math.floor(seconds / 86400); 16 | let hour = Math.floor(seconds % 86400 / 3600); 17 | let minute = Math.floor(seconds % 3600 / 60); 18 | let second = seconds % 60; 19 | 20 | return StdUtil.format('%d天%d时%d分%d秒', day, hour, minute, second); 21 | } 22 | 23 | /** 24 | * 25 | * @param timestamp 26 | * @returns {*} 27 | * @private 28 | */ 29 | /* 30 | function _formatUpTime(timestamp) { 31 | return moment(timestamp).format('DD天HH时mm分ss秒'); 32 | } 33 | */ 34 | 35 | /** 36 | * Module Exports 37 | */ 38 | exports.formatUpTime = _formatUpTime; 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Sentinel UI 2 | 3 | ## 部署运行: 4 | 5 | 1. `git clone https://github.com/youngsterxyf/redis-sentinel-ui.git` 6 | 2. `cd redis-sentinel-ui && npm install` 7 | 3. `gulp default` 8 | 4. 数据库初始化: `node init.js` 9 | 5. 启动后台redis监控数据收集进程: `node god.js` 10 | 6. 启用web应用进程: `node app.js` 11 | 12 | ## 截图 13 | 14 |  15 |  16 |  17 |  18 |  19 |  20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-sentinel-ui", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./app", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "youngsterxyf", 11 | "contributors": [ 12 | "gejiawen <806717031@qq.com>" 13 | ], 14 | "license": "ISC", 15 | "dependencies": { 16 | "bootstrap": "^3.3.6", 17 | "cn-bootstrap-datetimepicker": "^4.15.36", 18 | "co-body": "^4.0.0", 19 | "debug": "^2.2.0", 20 | "highcharts": "^4.2.0", 21 | "ioredis": "^1.11.1", 22 | "jade": "^1.11.0", 23 | "jquery": ">=1.8.3 <2.2.0", 24 | "log": "^1.4.0", 25 | "mime-types": "^2.1.8", 26 | "sqlite3": "^3.1.1" 27 | }, 28 | "devDependencies": { 29 | "browserify": "^12.0.1", 30 | "gulp": "^3.9.0", 31 | "gulp-browserify": "^0.5.1" 32 | }, 33 | "engines": { 34 | "node": ">=5.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiayf on 15/12/4. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const gulp = require('gulp'); 8 | const browserify = require('gulp-browserify'); 9 | 10 | gulp.task('browserify', () => { 11 | // browserify编译 12 | gulp.src('./public/main.js') 13 | .pipe(browserify()) 14 | .pipe(gulp.dest('./public/dist')); 15 | }); 16 | 17 | gulp.task('clone', () => { 18 | gulp.src([ 19 | './node_modules/bootstrap/dist/css/**/*.*', 20 | './node_modules/bootstrap/dist/fonts/**/*.*' 21 | ], {base: './node_modules/bootstrap/dist'}) 22 | .pipe(gulp.dest('./public/vendor/bootstrap')); 23 | 24 | gulp.src([ 25 | './node_modules/cn-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css' 26 | ], {base: './node_modules/cn-bootstrap-datetimepicker/build/css'}) 27 | .pipe(gulp.dest('./public/vendor')); 28 | }); 29 | 30 | gulp.task('default', ['browserify', 'clone']); 31 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: config 3 | * @author: gejiawen 4 | * @date: 15/12/4 17:09 5 | * @description: 6 | * 7 | * configuration 8 | * 9 | */ 10 | 11 | 'use strict'; 12 | 13 | const PORT = 8080; 14 | const STORAGE_FILE_PATH = './rsm.db'; 15 | 16 | let configuration = { 17 | debug: false, 18 | log_level: 'debug', 19 | port: PORT, 20 | static_prefix: '/public/', 21 | sentinels: [ 22 | { 23 | host: '127.0.0.1', 24 | port: '26379' 25 | }, { 26 | host: '127.0.0.1', 27 | port: '26389' 28 | }, { 29 | host: '127.0.0.1', 30 | port: '26399' 31 | } 32 | ], 33 | master_name: 'mymaster', 34 | auth: null, 35 | storage_file: STORAGE_FILE_PATH, 36 | sentinel_status_interval: 30000, 37 | cluster_info_interval: 120000, 38 | server_stat: 5000 39 | }; 40 | 41 | 42 | /** 43 | * Module Exports 44 | */ 45 | 46 | module.exports = configuration; 47 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | //- 2 | @file: layout 3 | @author: youngsterxyf, gejiawen 4 | @date: 15/12/5 19:14 5 | @description: layout 6 | 7 | - var date = new Date() 8 | - title = 'Redis Sentinel Manager' 9 | 10 | doctype html 11 | 12 | html 13 | head 14 | meta(charset='utf-8') 15 | title #{title} 16 | meta(name="viewport", content="width=1000, initial-scale=1.0, maximum-scale=1.0") 17 | link(rel='shortcut icon', href='/public/favicon.png') 18 | link(rel='stylesheet', href='/public/vendor/bootstrap/css/bootstrap.min.css') 19 | link(rel='stylesheet', href='/public/vendor/bootstrap/css/bootstrap-theme.min.css') 20 | link(rel='stylesheet', href='/public/vendor/bootstrap-datetimepicker.min.css') 21 | link(rel='stylesheet', href='/public/main.css') 22 | block head_css 23 | block head_js 24 | body 25 | div(class="container") 26 | include header 27 | block content 28 | include footer 29 | script(src='/public/dist/main.js') -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // store global var 6 | global.RootDir = __dirname; 7 | 8 | let cluster = require('cluster'); 9 | let numCPUs = require('os').cpus().length; 10 | let http = require('http'); 11 | 12 | let config = require('./config'); 13 | let routes = require('./routes'); 14 | let Logger = require('./utils/logger'); 15 | 16 | // 配置检查 17 | if (!config.sentinels.length) { 18 | Logger.error('请配置sentinel服务器'); 19 | } 20 | 21 | if (cluster.isMaster) { 22 | // Fork workers. 23 | Logger.info('主进程id: %d', process.pid); 24 | for (let i = 0; i < numCPUs; i++) { 25 | cluster.fork(); 26 | } 27 | 28 | cluster.on('exit', function(worker, code, signal) { 29 | Logger.info('worker ' + worker.process.pid + ' died; code: ' + code + ', signal: ' + signal); 30 | cluster.fork(); 31 | }); 32 | } else { 33 | // Workers can share any TCP connection 34 | // In this case it is an HTTP server 35 | http.createServer(routes).listen(config.port); 36 | Logger.info('进程id:', process.pid, '监听端口:', config.port); 37 | } 38 | -------------------------------------------------------------------------------- /controllers/stat_page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Template = require('../utils/template'); 4 | let DB = require('../models/db'); 5 | let Logger = require('../utils/logger'); 6 | 7 | let serverIndexs = { 8 | cmd_ps: '每秒处理命令数', 9 | connected_client: '客户端连接数', 10 | used_memory: '内存使用量' 11 | }; 12 | 13 | let reduceWays = { 14 | default: '不聚合(时间范围大时慎重使用)', 15 | by_max: '最大值', 16 | by_ave: '均值' 17 | }; 18 | 19 | function _stat_page(req, res) { 20 | DB.getClusterInfo(function (err, result) { 21 | if (err) { 22 | Logger.error(err); 23 | 24 | res.statusCode = 500; 25 | res.write('系统异常,请联系管理员'); 26 | res.end(); 27 | return; 28 | } 29 | 30 | let redisServers = []; 31 | let redisMaster = JSON.parse(result.master); 32 | let redisSlaves = JSON.parse(result.slaves); 33 | 34 | redisServers.push(redisMaster.ip + ':' + redisMaster.port); 35 | Object.getOwnPropertyNames(redisSlaves).forEach(slave => { 36 | redisServers.push(slave); 37 | }); 38 | 39 | res.write(Template.render('views/stat.jade', { 40 | servers: redisServers, 41 | indexs: serverIndexs, 42 | ways: reduceWays 43 | })); 44 | res.end(); 45 | }); 46 | } 47 | 48 | module.exports = _stat_page; 49 | -------------------------------------------------------------------------------- /views/stat.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h4 选择服务器: 5 | each server in servers 6 | div(class=["checkbox", "server"]) 7 | label 8 | input(type="checkbox", value="#{server}" checked=true) 9 | span #{server} 10 | h4 选择指标: 11 | each v, k in indexs 12 | div(class=["checkbox", "index"]) 13 | label 14 | - var isCMDPS = k === 'cmd_ps' 15 | input(type="checkbox", value="#{k}" checked=isCMDPS) 16 | span #{v} 17 | h4 选择聚合方式: 18 | each v, k in ways 19 | div(class="radio") 20 | label 21 | - var isDefault = k === 'default' 22 | input(type="radio", name="reduce_way", value="#{k}" checked=isDefault) 23 | span #{v} 24 | h4 选择时间范围: 25 | div(class="form-inline") 26 | div(class=["input-group", "date"], id="begin_datetime") 27 | input(type="text", class="form-control", name="begin-datetime") 28 | span(class="input-group-addon") 29 | span(class=["glyphicon", "glyphicon-calendar"]) 30 | div(class=["input-group", "date"], id="end_datetime") 31 | input(type="text", class="form-control", name="end-datetime") 32 | span(class="input-group-addon") 33 | span(class=["glyphicon", "glyphicon-calendar"]) 34 | button(type="submit", class=["btn", "btn-primary"], id="show_stat") 提交 35 | 36 | div(class="stat-graph-part") 37 | -------------------------------------------------------------------------------- /controllers/cmd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: cmd 3 | * @author: youngsterxyf, gejiawen 4 | * @date: 15/12/5 18:00 5 | * @description: cmd 6 | */ 7 | 8 | 'use strict'; 9 | 10 | let Redis = require('ioredis'); 11 | let ValidRedisCMDs = require('ioredis/commands'); 12 | 13 | let cmdRespParser = require('../utils/cmdRespParser'); 14 | let config = require('../config'); 15 | 16 | // 检测 命令 是否有效 17 | function isValidCommand(cmd) { 18 | cmd = cmd.toLowerCase(); 19 | return !!(cmd in ValidRedisCMDs); 20 | } 21 | 22 | /** 23 | * 24 | * @param req 25 | * @param res 26 | * @private 27 | */ 28 | 29 | function cmd(req, res) { 30 | /** 31 | * 请求参数: 32 | * 1. cmd: 大写的Redis命令 33 | * 2. params: 命令参数, 多个参数以空格分隔 34 | */ 35 | let cmd = req.body.cmd.toUpperCase(); 36 | if (!cmd || !isValidCommand(cmd)) { 37 | res.toResponse('参数cmd不合法!', 400); 38 | return; 39 | } 40 | let params = req.body.params; 41 | if (params === undefined) { 42 | res.toResponse('缺少必要的请求参数!', 400); 43 | return; 44 | } 45 | params = params.split(' '); 46 | 47 | let RedisServer = new Redis({ 48 | sentinels: config.sentinels, 49 | name: config.master_name, 50 | password: config.auth 51 | }); 52 | 53 | RedisServer[cmd.toLocaleLowerCase()].apply(RedisServer, params).then(function(result) { 54 | if (cmd === 'INFO') { 55 | result = cmdRespParser.infoRespParser(result.split('\r\n')); 56 | } 57 | RedisServer.disconnect(); 58 | 59 | res.toResponse(JSON.stringify(result, null, " ")); 60 | }, function(err) { 61 | res.toResponse(JSON.stringify(err, null, " ")); 62 | }); 63 | } 64 | 65 | /** 66 | * Module Exports 67 | */ 68 | module.exports = cmd; 69 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Created by xiayf on 15/12/24. 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let config = require('./config'); 9 | let sqlite3 = require('sqlite3').verbose(); 10 | 11 | let db = new sqlite3.Database(config.storage_file); 12 | 13 | /** 14 | * 初始化sqlite数据表 15 | */ 16 | let create_sentinels_sql = ` 17 | CREATE TABLE IF NOT EXISTS sentinels ( 18 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 19 | sentinel TEXT NOT NULL UNIQUE, 20 | status TEXT NOT NULL DEFAULT 'OFF' 21 | ); 22 | `; 23 | db.serialize(function() { 24 | db.run(create_sentinels_sql); 25 | db.run('DELETE FROM sentinels'); 26 | }); 27 | 28 | let create_clusterinfo_sql = ` 29 | CREATE TABLE IF NOT EXISTS cluster_info ( 30 | master_name TEXT NOT NULL UNIQUE, 31 | master TEXT NOT NULL DEFAULT '{}', 32 | slaves TEXT NOT NULL DEFAULT '{}', 33 | sentinels TEXT NOT NULL DEFAULT '{}' 34 | ) 35 | `; 36 | db.serialize(function() { 37 | db.run(create_clusterinfo_sql); 38 | db.run('DELETE FROM cluster_info'); 39 | db.run('INSERT INTO `cluster_info` (`master_name`) VALUES (?)', config.master_name); 40 | }); 41 | 42 | let create_connected_client = ` 43 | CREATE TABLE IF NOT EXISTS connected_client ( 44 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 45 | server TEXT NOT NULL, 46 | client_num INTEGER NOT NULL, 47 | created_time NOT NULL DEFAULT (datetime('now','localtime')) 48 | ); 49 | `; 50 | db.run(create_connected_client); 51 | // 手动添加个索引吧 52 | // CREATE INDEX connected_client_server_idx ON connected_client (server); 53 | 54 | let create_used_memory = ` 55 | CREATE TABLE IF NOT EXISTS used_memory ( 56 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 57 | server TEXT NOT NULL, 58 | used_memory REAL NOT NULL, 59 | created_time NOT NULL DEFAULT (datetime('now','localtime')) 60 | ); 61 | `; 62 | db.run(create_used_memory); 63 | // 手动添加个索引吧 64 | // CREATE INDEX used_memory_server_idx ON used_memory (server); 65 | 66 | let create_cmd_per_second = ` 67 | CREATE TABLE IF NOT EXISTS cmd_ps ( 68 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 69 | server TEXT NOT NULL, 70 | cmd_ps INTEGER NOT NULL, 71 | created_time NOT NULL DEFAULT (datetime('now','localtime')) 72 | ) 73 | `; 74 | db.run(create_cmd_per_second); 75 | // 手动添加索引 76 | // CREATE INDEX cmd_ps_server_idx ON cmd_ps (server); 77 | 78 | db.close(); -------------------------------------------------------------------------------- /models/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: db 3 | * @author: youngsterxyf, gejiawen 4 | * @date: 15/12/5 16:56 5 | * @description: 6 | * 7 | * 连接sqlite数据的model层 8 | * 9 | */ 10 | 11 | 'use strict'; 12 | 13 | let config = require('../config'); 14 | let sqlite3 = require('sqlite3').verbose(); 15 | 16 | // 只读模式 17 | let db = new sqlite3.Database(config.storage_file, sqlite3.OPEN_READONLY); 18 | 19 | /** 20 | * 从数据库中获取某个sentinel的状态 21 | * 22 | * @param sentinel_addr 23 | * @param callback 24 | * @private 25 | */ 26 | function _getSentinelPreviousStatus(sentinel_addr, callback) { 27 | db.get('SELECT status FROM `sentinels` WHERE sentinel=?', 28 | sentinel_addr, 29 | callback 30 | ); 31 | } 32 | 33 | /** 34 | * 从数据库中随便获取一个可用sentinel的地址 35 | * 36 | * @param callback 37 | * @private 38 | */ 39 | function _getActiveSentinel(callback) { 40 | db.get('SELECT sentinel FROM `sentinels` WHERE status="ON"', 41 | callback 42 | ); 43 | } 44 | 45 | function _getClusterInfo(callback) { 46 | let masterName = config.master_name; 47 | db.get('SELECT master, slaves, sentinels FROM `cluster_info` WHERE master_name=?', 48 | masterName, callback 49 | ); 50 | } 51 | 52 | function _getRangeConnectedClient(servers, beginTime, endTime, callback) { 53 | let serverCount = servers.length; 54 | 55 | let sql = 'SELECT client_num AS value, server, created_time FROM `connected_client` WHERE server IN (?'; 56 | while (serverCount > 1) { 57 | sql = sql + ', ?'; 58 | serverCount = serverCount - 1; 59 | } 60 | sql += ') AND created_time>=? AND created_time<=? ORDER BY created_time'; 61 | 62 | let stmtParams = servers; 63 | stmtParams.unshift(sql); 64 | stmtParams.push(beginTime); 65 | stmtParams.push(endTime); 66 | stmtParams.push(callback); 67 | 68 | db.all.apply(db, stmtParams); 69 | } 70 | 71 | function _getRangeUsedMemory(servers, beginTime, endTime, callback) { 72 | let serverCount = servers.length; 73 | 74 | let sql = 'SELECT used_memory AS value, server, created_time FROM `used_memory` WHERE server IN (?'; 75 | while (serverCount > 1) { 76 | sql = sql + ', ?'; 77 | serverCount = serverCount - 1; 78 | } 79 | sql += ') AND created_time>=? AND created_time<=? ORDER BY created_time'; 80 | 81 | let stmtParams = servers; 82 | stmtParams.unshift(sql); 83 | stmtParams.push(beginTime); 84 | stmtParams.push(endTime); 85 | stmtParams.push(callback); 86 | 87 | db.all.apply(db, stmtParams); 88 | } 89 | 90 | function _getRangeCMDPS(servers, beginTime, endTime, callback) { 91 | let serverCount = servers.length; 92 | 93 | let sql = 'SELECT cmd_ps AS value, server, created_time FROM `cmd_ps` WHERE server IN (?'; 94 | while (serverCount > 1) { 95 | sql = sql + ', ?'; 96 | serverCount = serverCount - 1; 97 | } 98 | sql += ') AND created_time>=? AND created_time<=? ORDER BY created_time'; 99 | 100 | let stmtParams = servers; 101 | stmtParams.unshift(sql); 102 | stmtParams.push(beginTime); 103 | stmtParams.push(endTime); 104 | stmtParams.push(callback); 105 | 106 | db.all.apply(db, stmtParams); 107 | } 108 | 109 | /** 110 | * Module Exports 111 | */ 112 | exports.getPrev = _getSentinelPreviousStatus; 113 | exports.getActive = _getActiveSentinel; 114 | exports.getClusterInfo = _getClusterInfo; 115 | exports.getRangeConnectedClient = _getRangeConnectedClient; 116 | exports.getRangeUsedMemory = _getRangeUsedMemory; 117 | exports.getRangeCMDPS = _getRangeCMDPS; 118 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: routes.js 3 | * @author: youngsterxyf, gejiawen 4 | * @date: 15/12/4 17:19 5 | * @description: routes.js 6 | */ 7 | 8 | 'use strict'; 9 | 10 | let fs = require('fs'); 11 | let urlParser = require('url'); 12 | //const qs = require('querystring'); 13 | 14 | let Parser = require('co-body'); 15 | 16 | let controllers = require('./controllers'); 17 | let StaticServ = require('./utils/staticServ'); 18 | let RespUtil = require('./utils/resp'); 19 | let Logger = require('./utils/logger'); 20 | 21 | // Web路由 22 | let routes = { 23 | '/': { 24 | 'verb': ['GET'], 25 | 'action': controllers.Home 26 | }, 27 | '/cmd_page': { 28 | 'verb': ['GET'], 29 | 'action': controllers.Cmd_page 30 | }, 31 | '/cmd': { 32 | 'verb': ['POST'], 33 | 'action': controllers.Cmd 34 | }, 35 | '/stat_page': { 36 | 'verb': ['GET'], 37 | 'action': controllers.Stat_page 38 | }, 39 | '/stat': { 40 | 'verb': ['POST'], 41 | 'action': controllers.Stat 42 | } 43 | }; 44 | 45 | function* _parseReqBody(req) { 46 | let type = 'form'; 47 | let body; 48 | 49 | let headers = req.headers; 50 | if ('content-type' in headers) { 51 | if (headers['content-type'] === 'application/json') { 52 | type = 'json'; 53 | } 54 | } 55 | if (type === 'json') { 56 | body = yield Parser.json(req); 57 | } else { 58 | body = yield Parser.form(req); 59 | } 60 | return body; 61 | } 62 | 63 | function _router(req, res) { 64 | let urlParts = urlParser.parse(req.url, true); 65 | let pathname = urlParts.pathname; 66 | 67 | Logger.info('%s %s', req.method, pathname); 68 | 69 | // 输出请求路径及方法 70 | // console.log(pathname, req.method); 71 | 72 | // 绑定一些方法 73 | res.toResponse = RespUtil.toRespone; 74 | 75 | // 匹配路由表 76 | if ((pathname in routes) && (routes[pathname].verb.indexOf(req.method) != -1)) { 77 | // 统一解析保存URL查询字符串的请求参数 78 | // req.query = qs.parse(urlParts.query); 79 | req.query = urlParts.query; 80 | 81 | if (req.method === 'POST' || req.method === 'PUT') { 82 | // 统一解析并保存请求体数据 83 | _parseReqBody(req).next().value.then(function(parsedBody) { 84 | req.body = parsedBody; 85 | // console.log(req.body); 86 | routes[pathname].action(req, res); 87 | }); 88 | } else { 89 | req.body = {}; 90 | // 执行对应的处理方法 91 | routes[pathname].action(req, res); 92 | } 93 | return; 94 | } 95 | 96 | StaticServ(pathname, function(err, result) { 97 | if (err) { 98 | Logger.error(err); 99 | 100 | res.toResponse(err.msg, err.code); 101 | return; 102 | } 103 | if (result.cached) { 104 | let data = result.data; 105 | 106 | let statusCode = 200; 107 | // 写响应头, 以及可能相应304 108 | let ifNoneMatch = req.headers['if-none-match']; 109 | if (ifNoneMatch && ifNoneMatch == data.md5) { 110 | statusCode = 304; 111 | } 112 | 113 | res.statusCode = statusCode; 114 | res.setHeader('Cache-Control', data.cacheControl || 'public, max-age=' + data.maxAge); 115 | res.setHeader('ETag', data.md5); 116 | res.setHeader('Expires', (new Date(Date.now() + data.maxAge * 1000)).toUTCString()); 117 | 118 | if (statusCode === 200) { 119 | res.write(data.buffer); 120 | } 121 | } else { 122 | res.write(result.data); 123 | } 124 | res.end(); 125 | }); 126 | } 127 | 128 | 129 | /** 130 | * Module Exports. 131 | */ 132 | module.exports = _router; 133 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: home 3 | * @author: youngsterxyf, gejiawen 4 | * @date: 15/12/5 18:00 5 | * @description: home 6 | */ 7 | 8 | 'use strict'; 9 | 10 | let DB = require('../models/db'); 11 | let Template = require('../utils/template'); 12 | let Time = require('../utils/time'); 13 | let Logger = require('../utils/logger'); 14 | 15 | /** 16 | * `/home`控制器 17 | * @param req 18 | * @param res 19 | * @private 20 | */ 21 | function _home(req, res) { 22 | DB.getClusterInfo(function(err, result) { 23 | if (err) { 24 | Logger.error(err); 25 | 26 | res.statusCode = 500; 27 | res.write('系统异常,请联系管理员'); 28 | res.end(); 29 | return; 30 | } 31 | 32 | if (!result) { 33 | Logger.info('Has no cluster info'); 34 | 35 | result = {master: '{}', slaves: '{}', sentinels: '{}'}; 36 | } 37 | 38 | let clusterInfo = { 39 | master: JSON.parse(result.master), 40 | slaves: JSON.parse(result.slaves), 41 | sentinels: JSON.parse(result.sentinels) 42 | }; 43 | 44 | let allSentinel = []; 45 | let redisSentinels = clusterInfo.sentinels; 46 | 47 | if (redisSentinels) { 48 | Object.getOwnPropertyNames(redisSentinels).forEach(ele => { 49 | let thisSentinel = redisSentinels[ele]; 50 | allSentinel.push({ 51 | address: ele, 52 | version: thisSentinel.redis_version, 53 | process_id: thisSentinel.process_id, 54 | pending_cmds: thisSentinel["pending-commands"], 55 | uptime: Time.formatUpTime(thisSentinel.uptime_in_seconds) 56 | }); 57 | }); 58 | } 59 | 60 | let allRedis = []; 61 | 62 | let rawRedisServers = {}; 63 | let redisMaster = clusterInfo.master; 64 | if (redisMaster && Object.getOwnPropertyNames(redisMaster).length) { 65 | rawRedisServers[redisMaster.ip + ':' + redisMaster.port] = redisMaster; 66 | } 67 | let redisSlaves = clusterInfo.slaves; 68 | if (redisSlaves) { 69 | rawRedisServers = Object.assign(rawRedisServers, redisSlaves); 70 | } 71 | 72 | Object.getOwnPropertyNames(rawRedisServers).forEach(function(ele) { 73 | let thisRedisServer = rawRedisServers[ele]; 74 | 75 | let hitRate = 0; 76 | let keySpaceHits = parseInt(thisRedisServer.keyspace_hits); 77 | let keySpaceMisses = parseInt(thisRedisServer.keyspace_misses); 78 | let keySpaceHitMisses = keySpaceHits + keySpaceMisses; 79 | if (keySpaceHitMisses > 0) { 80 | hitRate = (keySpaceHits / keySpaceHitMisses).toFixed(3); 81 | } 82 | 83 | allRedis.push({ 84 | address: ele, 85 | role: thisRedisServer.role, 86 | version: thisRedisServer.redis_version, 87 | process_id: thisRedisServer.process_id, 88 | used_memory: thisRedisServer.used_memory_human, 89 | pending_cmds: thisRedisServer["pending-commands"], 90 | uptime: Time.formatUpTime(thisRedisServer.uptime_in_seconds), 91 | used_memory_peak: thisRedisServer.used_memory_peak_human, 92 | total_commands_processed: thisRedisServer.total_commands_processed, 93 | rejected_connections: thisRedisServer.rejected_connections, 94 | mem_fragmentation_ratio: thisRedisServer.mem_fragmentation_ratio, 95 | total_connections_received: thisRedisServer.total_connections_received, 96 | instantaneous_ops_per_sec: thisRedisServer.instantaneous_ops_per_sec, 97 | keyspace_hits: keySpaceHits, 98 | keyspace_misses: keySpaceMisses, 99 | hit_rate: hitRate, 100 | mem_allocator: thisRedisServer.mem_allocator, 101 | used_cpu_sys: thisRedisServer.used_cpu_sys, 102 | used_cpu_user: thisRedisServer.used_cpu_user, 103 | used_cpu_sys_children: thisRedisServer.used_cpu_sys_children, 104 | used_cpu_user_children: thisRedisServer.used_cpu_user_children 105 | }); 106 | }); 107 | 108 | let data = { 109 | sentinels: allSentinel, 110 | redises: allRedis 111 | }; 112 | 113 | res.write(Template.render('views/home.jade', data)); 114 | res.end(); 115 | }); 116 | } 117 | 118 | /** 119 | * Module Exports 120 | */ 121 | module.exports = _home; 122 | -------------------------------------------------------------------------------- /views/home.jade: -------------------------------------------------------------------------------- 1 | //- 2 | @file: home 3 | @author: youngsterxyf,gejiawen 4 | @date: 15/12/5 19:15 5 | @description: home 6 | 7 | extends layout 8 | 9 | block content 10 | h3 Sentinel服务器 11 | each sentinel in sentinels 12 | div(class=['panel', 'panel-info']) 13 | div(class="panel-heading") #{sentinel.address} 14 | div(class="panel-body") 15 | div(class="container-fluid") 16 | div(class="row") 17 | div(class="col-md-6") 18 | label 版本: 19 | span #{sentinel.version} 20 | div(class="col-md-6") 21 | label 进程ID: 22 | span #{sentinel.process_id} 23 | div(class="row") 24 | div(class="col-md-6") 25 | label 运行时长: 26 | span #{sentinel.uptime} 27 | div(class="col-md-6") 28 | label 等待执行命令数: 29 | span #{sentinel.pending_cmds} 30 | 31 | h3 Redis服务器 32 | each redis in redises 33 | - var isMaster = redis.role === 'master' 34 | div(class=['panel', 'panel-success']) 35 | div(class="panel-heading") 36 | span #{redis.address} 37 | label(class=isMaster ? "label label-primary" : "label label-info") #{redis.role} 38 | div(class="panel-body") 39 | div(class="container-fluid") 40 | div(class="row") 41 | div(class="col-md-6") 42 | label 版本: 43 | span #{redis.version} 44 | div(class="col-md-6") 45 | label 进程ID: 46 | span #{redis.process_id} 47 | div(class="row") 48 | div(class="col-md-6") 49 | label 运行时长: 50 | span #{redis.uptime} 51 | div(class="col-md-6") 52 | label 内存占用: 53 | span #{redis.used_memory} 54 | div(class="row") 55 | div(class="col-md-6") 56 | label 内存分配器: 57 | span #{redis.mem_allocator} 58 | div(class="col-md-6") 59 | label 内存碎片比: 60 | span #{redis.mem_fragmentation_ratio} 61 | div(class="row") 62 | div(class="col-md-6") 63 | label 进程系统态CPU时间: 64 | span #{redis.used_cpu_sys}秒 65 | div(class="col-md-6") 66 | label 进程用户态CPU时间: 67 | span #{redis.used_cpu_user}秒 68 | div(class="row") 69 | div(class="col-md-6") 70 | label 后台子进程系统态CPU时间: 71 | span #{redis.used_cpu_sys_children}秒 72 | div(class="col-md-6") 73 | label 后台子进程用户态CPU时间: 74 | span #{redis.used_cpu_user_children}秒 75 | div(class="row") 76 | div(class="col-md-6") 77 | label 等待执行命令数: 78 | span #{redis.pending_cmds} 79 | div(class="col-md-6") 80 | label 内存占用峰值: 81 | span #{redis.used_memory_peak} 82 | div(class="row") 83 | div(class="col-md-6") 84 | label 已接连接总数: 85 | span #{redis.total_connections_received} 86 | div(class="col-md-6") 87 | label 拒绝的连接数: 88 | span #{redis.rejected_connections} 89 | div(class="row") 90 | div(class="col-md-6") 91 | label 已处理命令数: 92 | span #{redis.total_commands_processed} 93 | div(class="col-md-6") 94 | label 每秒处理命令数: 95 | span #{redis.instantaneous_ops_per_sec} 96 | div(class="row") 97 | div(class="col-md-6") 98 | label 查询命中数: 99 | span #{redis.keyspace_hits} 100 | div(class="col-md-6") 101 | label 查询未命中数: 102 | span #{redis.keyspace_misses} 103 | div(class="row") 104 | div(class="col-md-6") 105 | label 查询命中率: 106 | span #{redis.hit_rate} -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiayf on 15/12/4. 3 | */ 4 | let $ = jQuery = require('jquery'); 5 | let _ = require('bootstrap'); 6 | let __ = require('cn-bootstrap-datetimepicker'); 7 | let Highcharts = require('highcharts'); 8 | Highcharts.setOptions({ global: { useUTC: false } }); 9 | 10 | let statTitleMapper = { 11 | 'connected_client': '客户端连接数(个)', 12 | 'used_memory': '内存使用量(MB)', 13 | 'cmd_ps': '每秒处理命令数(个)' 14 | }; 15 | 16 | function genErrorAlert(xhr) { 17 | return '
' + xhr.responseText + '
' 19 | + '' + resp + ''; 70 | $cmdOutput.html(resp); 71 | }); 72 | req.fail(function(xhr) { 73 | $loadingPart.remove(); 74 | $cmdOutput.append(genErrorAlert(xhr)); 75 | }); 76 | }); 77 | 78 | $('#show_stat').on('click', function ($e) { 79 | $e.preventDefault(); 80 | 81 | let selectedServer = []; 82 | $('.server input').each(function () { 83 | if ($(this).prop('checked')) { 84 | selectedServer.push($(this).val()); 85 | } 86 | }); 87 | 88 | let selectedIndex = []; 89 | $('.index input').each(function () { 90 | if ($(this).prop('checked')) { 91 | selectedIndex.push($(this).val()); 92 | } 93 | }); 94 | 95 | let reduceWay = $('input[name="reduce_way"]:checked').val(); 96 | 97 | let beginDateTime = $('input[name="begin-datetime"]').val(); 98 | let endDateTime = $('input[name="end-datetime"]').val(); 99 | 100 | let $statGraphPart = $('.stat-graph-part'); 101 | $statGraphPart.empty(); 102 | 103 | if (selectedServer.length === 0 || selectedIndex.length === 0 || !beginDateTime || !endDateTime) { 104 | $statGraphPart.append('