├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── design ├── overview.png └── redis-live.png ├── install └── centos │ ├── install-redis-monitor.sh │ ├── redis-monitor │ └── redis7009.conf ├── requirements.txt ├── specification_ch.txt └── src ├── __init__.py ├── api ├── __init__.py ├── controller │ ├── BaseController.py │ ├── BaseStaticFileHandler.py │ ├── CommandsController.py │ ├── InfoController.py │ ├── InfoListController.py │ ├── MemoryController.py │ ├── ServerListController.py │ ├── SettingsController.py │ ├── SlowlogController.py │ ├── StatusController.py │ ├── TopCommandsController.py │ ├── TopKeysController.py │ └── __init__.py └── util │ ├── RDP.py │ ├── __init__.py │ └── settings.py ├── daemonized.py ├── dataprovider ├── __init__.py ├── dataprovider.py └── redisprovider.py ├── redis_live.conf ├── redis_live.py ├── redis_live_daemon.py ├── redis_monitor.py ├── redis_monitor_daemon.py └── www ├── images └── logo.png ├── index.html ├── js ├── app.js ├── libs │ ├── backbone │ │ └── backbone-min.js │ ├── bootstrap │ │ ├── bootstrap.css │ │ ├── img │ │ │ ├── glyphicons-halflings-white.png │ │ │ └── glyphicons-halflings.png │ │ ├── js │ │ │ ├── README.md │ │ │ ├── bootstrap-alert.js │ │ │ ├── bootstrap-button.js │ │ │ ├── bootstrap-carousel.js │ │ │ ├── bootstrap-collapse.js │ │ │ ├── bootstrap-dropdown.js │ │ │ ├── bootstrap-modal.js │ │ │ ├── bootstrap-popover.js │ │ │ ├── bootstrap-scrollspy.js │ │ │ ├── bootstrap-tab.js │ │ │ ├── bootstrap-tooltip.js │ │ │ ├── bootstrap-transition.js │ │ │ ├── bootstrap-typeahead.js │ │ │ └── tests │ │ │ │ ├── index.html │ │ │ │ ├── unit │ │ │ │ ├── bootstrap-alert.js │ │ │ │ ├── bootstrap-button.js │ │ │ │ ├── bootstrap-collapse.js │ │ │ │ ├── bootstrap-dropdown.js │ │ │ │ ├── bootstrap-modal.js │ │ │ │ ├── bootstrap-popover.js │ │ │ │ ├── bootstrap-scrollspy.js │ │ │ │ ├── bootstrap-tab.js │ │ │ │ ├── bootstrap-tooltip.js │ │ │ │ ├── bootstrap-transition.js │ │ │ │ └── bootstrap-typeahead.js │ │ │ │ └── vendor │ │ │ │ ├── jquery.js │ │ │ │ ├── qunit.css │ │ │ │ └── qunit.js │ │ └── less │ │ │ ├── accordion.less │ │ │ ├── alerts.less │ │ │ ├── badges.less │ │ │ ├── bootstrap.less │ │ │ ├── breadcrumbs.less │ │ │ ├── button-groups.less │ │ │ ├── buttons.less │ │ │ ├── carousel.less │ │ │ ├── close.less │ │ │ ├── code.less │ │ │ ├── component-animations.less │ │ │ ├── dropdowns.less │ │ │ ├── forms.less │ │ │ ├── grid.less │ │ │ ├── hero-unit.less │ │ │ ├── labels.less │ │ │ ├── layouts.less │ │ │ ├── mixins.less │ │ │ ├── modals.less │ │ │ ├── navbar.less │ │ │ ├── navs.less │ │ │ ├── pager.less │ │ │ ├── pagination.less │ │ │ ├── popovers.less │ │ │ ├── progress-bars.less │ │ │ ├── reset.less │ │ │ ├── responsive.less │ │ │ ├── scaffolding.less │ │ │ ├── sprites.less │ │ │ ├── tables.less │ │ │ ├── thumbnails.less │ │ │ ├── tooltip.less │ │ │ ├── type.less │ │ │ ├── utilities.less │ │ │ ├── variables.less │ │ │ ├── wells.less │ │ │ └── widget.less │ ├── corechart.js │ ├── google.js │ ├── handlebars │ │ └── handlebars-1.0.0.beta.6.js │ ├── jquery │ │ └── jquery-1.7.2.min.js │ ├── jsapi.js │ ├── less │ │ └── less-1.3.0.min.js │ ├── tooltip.css │ └── underscore │ │ └── underscore-min.js ├── models │ ├── commands-widget-model.js │ ├── info-widget-model.js │ ├── memory-widget-model.js │ ├── serverlist-model.js │ ├── status-widget-model.js │ ├── top-commands-widget-model.js │ └── top-keys-widget-model.js └── views │ ├── base-widget-view.js │ ├── commands-widget-view.js │ ├── info-widget-view.js │ ├── memory-widget-view.js │ ├── serverlist-view.js │ ├── status-widget-view.js │ ├── top-commands-widget-view.js │ └── top-keys-widget-view.js ├── overview.html ├── settings.html └── static ├── css ├── reset.css └── style.css └── js └── jquery.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | .DS_Store 30 | 31 | *.sublime-workspace -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nitin Kumar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to 8 | do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | 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 THE 16 | 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 IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NOTICE: This repository is no longer maintained, maybe some compatibility issue exists. 2 | 3 | redis-monitor 4 | --------- 5 | 6 | Base [RedisLive](https://github.com/nkrode/RedisLive) 7 | 8 | ## Features: 9 | * cluster: support thousands of redis instances 10 | * light: redis info base 11 | * metrics: memory, comands, Key HitRate, keyspace, master-slave change, expire keys 12 | * notification API: crash, master-slave stats changed notify 13 | 14 | ## Configuration 15 | vim src/redis_live.conf 16 | 17 | config: 18 | 19 | - RedisStatsServer: stats storage backend(redis) 20 | - others: config on dashboard settings tab 21 | 22 | samples: 23 | ``` 24 | {"master_slave_sms": "1,1", 25 | "RedisStatsServer": {"port": 6379, "server": "127.0.0.1"}, 26 | "sms_alert": "192.168.110.207:9999", 27 | "DataStoreType": "redis", 28 | "RedisServers": [ 29 | {"instance": "Master1", "group": "Test1", "port": 6379, "server": "127.0.0.1"}, 30 | {"instance": "Slave1", "group": "Test1", "port": 6380, "server": "127.0.0.1"} 31 | ]} 32 | 33 | ``` 34 | 35 | ## Install Deps 36 | pip install -r requirements.txt 37 | 38 | ## Run 39 | # 1. start redis instance for stat stroage 40 | redis-server --port 6379 41 | 42 | # 2. start web portal 43 | cd src/ 44 | python redis_live.py 45 | 46 | # 3. start stats collector daemon process 47 | cd src/ 48 | python redis_monitor.py 49 | 50 | # 4. dashboard: http://127.0.0.1:8888/index.html 51 | 52 | ## overview 53 | ![Redis Live](https://raw.github.com/LittlePeng/redis-monitor/master/design/redis-live.png) 54 | ![Redis Live](https://raw.github.com/LittlePeng/redis-monitor/master/design/overview.png) 55 | 56 | -------------------------------------------------------------------------------- /design/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/design/overview.png -------------------------------------------------------------------------------- /design/redis-live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/design/redis-live.png -------------------------------------------------------------------------------- /install/centos/install-redis-monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /usr/local/redis-monitor 4 | \cp -rf ../../* /usr/local/redis-monitor 5 | \cp -rf redis7009.conf /etc/redis/ 6 | \cp -rf redis-monitor /etc/init.d/ 7 | chmod 777 /etc/init.d/redis-monitor 8 | 9 | chkconfig --add redis-monitor 10 | service redis-monitor start 11 | 12 | -------------------------------------------------------------------------------- /install/centos/redis-monitor: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #chkconfig: 35 90 10 3 | #description: redis-monitor 4 | 5 | monitor_start() { 6 | monitor_stopmonitor 7 | /usr/local/bin/redis-server /etc/redis/redis7009.conf 8 | python /usr/local/redis-monitor/redis_monitor_daemon.py 9 | python /usr/local/redis-monitor/redis_live_daemon.py 10 | } 11 | 12 | monitor_stop() { 13 | monitor_stopmonitor 14 | monitor_stoplive 15 | /usr/local/bin/redis-cli -p 7009 SHUTDOWN 16 | } 17 | monitor_stopmonitor(){ 18 | MPID=`ps axu|grep "redis_monitor_daemon.py"|grep -v grep|awk '{ print $2}'` 19 | if [ "$MPID" != "" ]; 20 | then 21 | echo "kill monitor ( pid =" $MPID ")" 22 | kill -s KILL $MPID 23 | fi 24 | } 25 | monitor_stoplive(){ 26 | MPID=`ps axu|grep "redis_live_daemon.py"|grep -v grep|awk '{ print $2}'` 27 | if [ "$MPID" != "" ]; 28 | then 29 | echo "kill live ( pid =" $MPID ")" 30 | kill -s KILL $MPID 31 | fi 32 | } 33 | monitor_remon(){ 34 | monitor_stopmonitor 35 | python /usr/local/redis-monitor/redis_monitor_daemon.py 36 | } 37 | monitor_relive(){ 38 | monitor_stoplive 39 | python /usr/local/redis-monitor/redis_live_daemon.py 40 | } 41 | monitor_restart(){ 42 | monitor_stop 43 | monitor_start 44 | } 45 | monitor_usage() { 46 | echo -e "Usage: $0 {start,stop,restart,relive,remon}" 47 | exit 1 48 | } 49 | 50 | case "$1" in 51 | start) monitor_start ;; 52 | stop) monitor_stop ;; 53 | restart) monitor_restart ;; 54 | relive) monitor_relive ;; 55 | remon) monitor_remon ;; 56 | *) monitor_usage ;; 57 | esac 58 | -------------------------------------------------------------------------------- /install/centos/redis7009.conf: -------------------------------------------------------------------------------- 1 | pidfile /var/run/redis7009.pid 2 | port 7009 3 | dir ./ 4 | dbfilename dump7009.rdb 5 | appendfilename appendonly7009.aof 6 | logfile /var/log/redis7009.log 7 | 8 | timeout 300 9 | loglevel notice 10 | databases 16 11 | save "" 12 | daemonize yes 13 | stop-writes-on-bgsave-error yes 14 | rdbcompression no 15 | rdbchecksum yes 16 | maxclients 1000 17 | # 2G 18 | maxmemory 2147483648 19 | maxmemory-policy allkeys-lru 20 | maxmemory-samples 5 21 | appendonly yes 22 | appendfsync everysec 23 | no-appendfsync-on-rewrite no 24 | auto-aof-rewrite-percentage 0 25 | auto-aof-rewrite-min-size 64mb 26 | lua-time-limit 5000 27 | #10 ms 28 | slowlog-log-slower-than 10000 29 | slowlog-max-len 128 30 | hash-max-ziplist-entries 512 31 | hash-max-ziplist-value 64 32 | list-max-ziplist-entries 512 33 | list-max-ziplist-value 64 34 | set-max-intset-entries 512 35 | zset-max-ziplist-entries 128 36 | zset-max-ziplist-value 64 37 | activerehashing yes 38 | client-output-buffer-limit normal 0 0 0 39 | client-output-buffer-limit slave 1024mb 1024mb 60 40 | client-output-buffer-limit pubsub 32mb 8mb 60 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | redis 3 | tornado 4 | argparse 5 | -------------------------------------------------------------------------------- /specification_ch.txt: -------------------------------------------------------------------------------- 1 | 2 | 一、Monitor 3 | 1. 为每个redis实例开启一个定时线程,用于收集Info数据,并存储如下: 4 | - Info: 但获取失败是保存为 NULL值,并提取如下数据单独存储,便于显示 5 | - Memory :peak,current 6 | - command: 两次差值计算结果 7 | - Status : down、Master(detail)、slave(detail),并只有在两次变更时记录 8 | - Hit rate 9 | - keys、expires 10 | - 过期数据 expired、evicted 11 | 12 | 2.数据量评估 13 | - 如每2s取一次,那么每小时数据:1200,一次Info数据1.5K;那么一个实例1小时1.8M,那么20个实例一天产生数据:860M。 14 | - Info全量数据还是不准备全量保存一份,无论redis内存占用,还是使用sqllite,压力都大。 15 | - 那么不缓存Info,其他数据一次约50字节,那么一天下来:30M;设计最长保存7天;由Monitor定时检查。 16 | - redis采用ZSET保存,使用UNIX 时间戳为score,数据内容使用二进制保存。 17 | 18 | 二、Web界面: 19 | Overview + Live 20 | 21 | 三、使用方式 22 | - redis-live.conf 为json格式配置, 收集到的数据存储目前只能使用redis 23 | 24 | - 启动: 25 | 两个进程: redis-monitor.py 为定时收集进程; redis-live.py 是站点,端口为:8888 26 | 27 | 安装依赖: 28 | python2.7 29 | tornado 30 | redis-py 31 | python-dateutil-2.1 32 | jinja2 33 | werkzeug 34 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/__init__.py -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/controller/BaseController.py: -------------------------------------------------------------------------------- 1 | from dataprovider.dataprovider import RedisLiveDataProvider 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import redis 6 | 7 | class BaseController(tornado.web.RequestHandler): 8 | 9 | stats_provider = RedisLiveDataProvider.get_provider() 10 | 11 | def getStatsPerServer(self, server, password=None): 12 | try: 13 | connection = redis.Redis(host=server[0], port=(int)(server[1]), db=0, password=password, socket_timeout=0.1) 14 | info = connection.info() 15 | # when instances down ,this maybe slowly... 16 | info.update({ 17 | "server_name" : server, 18 | "status" : info.get("role"), 19 | "last_save_humanized": info.get("last_save_time") 20 | }) 21 | 22 | #master status 23 | role = info["role"] 24 | slaves="" 25 | 26 | if(role == "master"): 27 | connected_slaves = (int)(info["connected_slaves"]) 28 | slaves = "" 29 | for i in range(0, connected_slaves): 30 | slaves += str(info["slave" + (str)(i)]) 31 | else: 32 | master_host = info["master_host"] 33 | master_port = (str)(info["master_port"]) 34 | master_link_status = info["master_link_status"] 35 | master_sync_in_progress = info["master_sync_in_progress"] 36 | if(master_host!=""): 37 | slaves=master_host+":"+(str)(master_port)+","+master_link_status 38 | if(master_sync_in_progress==1): 39 | slaves+=",syncing" 40 | info['master_slaves']=slaves 41 | 42 | 43 | except redis.exceptions.ConnectionError: 44 | info = { 45 | "role" :"down", 46 | "uptime_in_seconds" :0, 47 | "total_commands_processed":0, 48 | "used_memory_human" :"", 49 | "connected_clients" :"", 50 | "status" : "down", 51 | "server_name" : server, 52 | "connected_clients" : 0, 53 | "used_memory_human" : '?', 54 | } 55 | 56 | return info 57 | 58 | def datetime_to_list(self, datetime): 59 | """Converts a datetime to a list. 60 | 61 | Args: 62 | datetime (datetime): The datetime to convert. 63 | """ 64 | parsed_date = dateutil.parser.parse(datetime) 65 | # don't return the last two fields, we don't want them. 66 | return tuple(parsed_date.timetuple())[:-2] 67 | 68 | # todo : fix this 69 | def average_data(self, data): 70 | """Averages data. 71 | 72 | TODO: More docstring here, once functionality is understood. 73 | """ 74 | average = [] 75 | 76 | deviation = 1024 * 1024 77 | 78 | start = dateutil.parser.parse(data[0][0]) 79 | end = dateutil.parser.parse(data[-1][0]) 80 | difference = end - start 81 | weeks, days = divmod(difference.days, 7) 82 | minutes, seconds = divmod(difference.seconds, 60) 83 | hours, minutes = divmod(minutes, 60) 84 | 85 | # TODO: These if/elif/else branches chould probably be broken out into 86 | # individual functions to make it easier to follow what's going on. 87 | if difference.days > 0: 88 | current_max = 0 89 | current_current = 0 90 | current_d = 0 91 | 92 | for dt, max_memory, current_memory in data: 93 | d = dateutil.parser.parse(dt) 94 | if d.day != current_d: 95 | current_d = d.day 96 | average.append([dt, max_memory, current_memory]) 97 | current_max = max_memory 98 | current_current = current_memory 99 | else: 100 | if max_memory > current_max or \ 101 | current_memory > current_current: 102 | average.pop() 103 | average.append([dt, max_memory, current_memory]) 104 | current_max = max_memory 105 | current_current = current_memory 106 | elif hours > 0: 107 | current_max = 0 108 | current_current = 0 109 | current = -1 110 | keep_flag = False 111 | 112 | for dt, max_memory, current_memory in data: 113 | d = dateutil.parser.parse(dt) 114 | if d.hour != current: 115 | current = d.hour 116 | average.append([dt, max_memory, current_memory]) 117 | current_max = max_memory 118 | current_current = current_memory 119 | keep_flag = False 120 | elif abs(max_memory - current_max) > deviation or \ 121 | abs(current_memory - current_current) > deviation: 122 | # average.pop() 123 | average.append([dt, max_memory, current_memory]) 124 | current_max = max_memory 125 | current_current = current_memory 126 | keep_flag = True 127 | elif max_memory > current_max or \ 128 | current_memory > current_current: 129 | if keep_flag != True: 130 | average.pop() 131 | average.append([dt, max_memory, current_memory]) 132 | current_max = max_memory 133 | current_current = current_memory 134 | keep_flag = False 135 | else: 136 | current_max = 0 137 | current_current = 0 138 | current_m = -1 139 | keep_flag = False 140 | for dt, max_memory, current_memory in data: 141 | d = dateutil.parser.parse(dt) 142 | if d.minute != current_m: 143 | current_m = d.minute 144 | average.append([dt, max_memory, current_memory]) 145 | current_max = max_memory 146 | current_current = current_memory 147 | keep_flag = False 148 | elif abs(max_memory - current_max) > deviation or \ 149 | abs(current_memory - current_current) > deviation: 150 | # average.pop() 151 | average.append([dt, max_memory, current_memory]) 152 | current_max = max_memory 153 | current_current = current_memory 154 | keep_flag = True 155 | elif max_memory > current_max or \ 156 | current_memory > current_current: 157 | if keep_flag != True: 158 | average.pop() 159 | average.append([dt, max_memory, current_memory]) 160 | current_max = max_memory 161 | current_current = current_memory 162 | keep_flag = False 163 | 164 | return average 165 | -------------------------------------------------------------------------------- /src/api/controller/BaseStaticFileHandler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | 3 | class BaseStaticFileHandler(tornado.web.StaticFileHandler): 4 | def compute_etag(self): 5 | return None 6 | 7 | def get_cache_time(self, path, modified, mime_type): 8 | return None 9 | 10 | -------------------------------------------------------------------------------- /src/api/controller/CommandsController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class CommandsController(BaseController): 9 | 10 | def get(self): 11 | """Serves a GET request. 12 | """ 13 | return_data = dict(data=[], 14 | timestamp=datetime.datetime.now().isoformat()) 15 | 16 | server = self.get_argument("server") 17 | from_date = self.get_argument("from", None) 18 | to_date = self.get_argument("to", None) 19 | 20 | if from_date==None or to_date==None or len(from_date)==0: 21 | end = datetime.datetime.now() 22 | delta = datetime.timedelta(seconds=900) 23 | start = end - delta 24 | else: 25 | start = dateutil.parser.parse(from_date) 26 | end = dateutil.parser.parse(to_date) 27 | 28 | data = self.stats_provider.get_keys_info(server, start, end) 29 | 30 | return_data['data']=data 31 | 32 | self.write(return_data) 33 | -------------------------------------------------------------------------------- /src/api/controller/InfoController.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from BaseController import BaseController 3 | import tornado.ioloop 4 | import tornado.web 5 | import re 6 | import redis 7 | from api.util import settings 8 | 9 | class InfoController(BaseController): 10 | def get(self): 11 | """Serves a GET request. 12 | """ 13 | 14 | server = self.get_argument("server").split(':') 15 | 16 | password = None 17 | 18 | for redis_server in settings.get_redis_servers(): 19 | if (redis_server["server"] == server[0]) and (int(redis_server["port"]) == int(server[1])): 20 | password = redis_server["password"] 21 | break 22 | 23 | redis_info = self.getStatsPerServer(server, password) 24 | databases=[] 25 | 26 | for key in sorted(redis_info.keys()): 27 | if key.startswith("db"): 28 | database = redis_info[key] 29 | database['name']=key 30 | databases.append(database) 31 | 32 | total_keys=0 33 | for database in databases: 34 | total_keys+=database.get("keys") 35 | 36 | if(total_keys==0): 37 | databases=[{"name" : "db0", "keys" : "0", "expires" : "0"}] 38 | 39 | redis_info['databases'] = databases 40 | redis_info['total_keys']= self.shorten_number(total_keys) 41 | 42 | uptime_seconds = redis_info['uptime_in_seconds'] 43 | redis_info['uptime'] = self.shorten_time(uptime_seconds) 44 | 45 | commands_processed = redis_info['total_commands_processed'] 46 | commands_processed = self.shorten_number(commands_processed) 47 | redis_info['total_commands_processed_human'] = commands_processed 48 | 49 | self.write(redis_info) 50 | 51 | def shorten_time(self, seconds): 52 | """Takes an integer number of seconds and rounds it to a human readable 53 | format. 54 | 55 | Args: 56 | seconds (int): Number of seconds to convert. 57 | """ 58 | if seconds < 60: 59 | # less than 1 minute 60 | val = str(seconds) + " sec" 61 | elif seconds < 3600: 62 | # if the seconds is less than 1hr 63 | num = self.rounded_number(seconds, 60) 64 | if num == "60": 65 | val = '1h' 66 | else: 67 | val = num + "m" 68 | elif (seconds < 60*60*24): 69 | # if the number is less than 1 day 70 | num = self.rounded_number(seconds, 60 * 60) 71 | if num == "24": 72 | val = "1d" 73 | else: 74 | val = num + "h" 75 | else: 76 | num = self.rounded_number(seconds, 60*60*24) 77 | val = num + "d" 78 | 79 | return val 80 | 81 | def shorten_number(self, number): 82 | """Shortens a number to a human readable format. 83 | 84 | Args: 85 | number (int): Number to convert. 86 | """ 87 | if number < 1000: 88 | return number 89 | elif number >= 1000 and number < 1000000: 90 | num = self.rounded_number(number, 1000) 91 | val = "1M" if num == "1000" else num + "K" 92 | return val 93 | elif number >= 1000000 and number < 1000000000: 94 | num = self.rounded_number(number, 1000000) 95 | val = "1B" if num=="1000" else num + "M" 96 | return val 97 | elif number >= 1000000000 and number < 1000000000000: 98 | num = self.rounded_number(number, 1000000000) 99 | val = "1T" if num=="1000" else num + "B" 100 | return val 101 | else: 102 | num = self.rounded_number(number, 1000000000000) 103 | return num + "T" 104 | 105 | def rounded_number(self, number, denominator): 106 | """Rounds a number. 107 | 108 | Args: 109 | number (int|float): The number to round. 110 | denominator (int): The denominator. 111 | """ 112 | rounded = str(round(Decimal(number)/Decimal(denominator), 1)) 113 | replace_trailing_zero = re.compile('0$') 114 | no_trailing_zeros = replace_trailing_zero.sub('', rounded) 115 | replace_trailing_period = re.compile('\.$') 116 | final_number = replace_trailing_period.sub('', no_trailing_zeros) 117 | return final_number 118 | -------------------------------------------------------------------------------- /src/api/controller/InfoListController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | from api.util import settings 3 | import redis 4 | 5 | class InfoListController(BaseController): 6 | 7 | def get(self): 8 | group = self.get_argument("group", None) 9 | 10 | response = {} 11 | response['data']=[] 12 | for server in settings.get_redis_servers(): 13 | if(group !=None and group!='all' and server['group'] != group): 14 | continue; 15 | 16 | info=self.getStatsPerServer((server['server'], server['port']), server['password']) 17 | 18 | info.update({"addr" : info.get("server_name")[0].replace(".", "_") + str(info.get("server_name")[1]), 19 | }) 20 | info['show_name']=server['group']+'('+server['instance']+')' 21 | info['group']= server['group'] 22 | screen_strategy = 'normal' 23 | if info.get("status") == 'down': 24 | screen_strategy = 'hidden' 25 | 26 | info.update({ "screen_strategy": screen_strategy,}) 27 | 28 | response["data"].append(info) 29 | 30 | self.write(response) 31 | -------------------------------------------------------------------------------- /src/api/controller/MemoryController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class MemoryController(BaseController): 9 | 10 | def get(self): 11 | server = self.get_argument("server") 12 | from_date = self.get_argument("from", None) 13 | to_date = self.get_argument("to", None) 14 | 15 | return_data = dict(data=[], 16 | timestamp=datetime.datetime.now().isoformat()) 17 | 18 | if from_date==None or to_date==None: 19 | end = datetime.datetime.now() 20 | delta = datetime.timedelta(seconds=60) 21 | start = end - delta 22 | else: 23 | start = dateutil.parser.parse(from_date) 24 | end = dateutil.parser.parse(to_date) 25 | 26 | combined_data = [] 27 | # TODO: These variables aren't currently used; should they be removed? 28 | prev_max=0 29 | prev_current=0 30 | counter=0 31 | 32 | for data in self.stats_provider.get_memory_info(server, start, end): 33 | combined_data.append([data[0], data[1], data[2]]) 34 | 35 | for data in combined_data: 36 | d = [self.datetime_to_list(data[0]), data[1], data[2]] 37 | return_data['data'].append(d) 38 | 39 | self.write(return_data) 40 | 41 | -------------------------------------------------------------------------------- /src/api/controller/ServerListController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | from api.util import settings 3 | 4 | class ServerListController(BaseController): 5 | 6 | def get(self): 7 | servers = {"servers": self.read_server_config()} 8 | self.write(servers) 9 | 10 | def read_server_config(self): 11 | server_list = [] 12 | redis_servers = settings.get_redis_servers() 13 | 14 | for server in redis_servers: 15 | server['id']= "%(server)s:%(port)s" % server 16 | server_list.append(server) 17 | 18 | return server_list 19 | -------------------------------------------------------------------------------- /src/api/controller/SettingsController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | from api.util import settings 3 | import os 4 | 5 | class SettingsController(BaseController): 6 | 7 | def get(self): 8 | server_list="" 9 | for server in settings.get_redis_servers(): 10 | server_list+= "%(server)s:%(port)s %(group)s %(instance)s\r\n" % server 11 | 12 | sms_repl=0; 13 | sms_stats=0; 14 | try: 15 | sms=settings.get_master_slave_sms_type() 16 | sms=sms.split(',') 17 | sms_repl=(int)(sms[0]) 18 | sms_stats=(int)(sms[1]) 19 | except: 20 | pass 21 | 22 | servers = {"servers": server_list,"sms1":sms_repl,"sms2":sms_stats} 23 | self.write(servers) 24 | 25 | def post(self): 26 | try: 27 | server_list=self.get_argument("servers") 28 | sms1=(int)(self.get_argument("sms1")) 29 | sms2=(int)(self.get_argument("sms2")) 30 | sms= "%s,%s" %(sms1,sms2) 31 | 32 | servers=[] 33 | for server in server_list.split('\n'): 34 | eps=server.split(':') 35 | if(len(eps)!=2): 36 | raise Exception('server Ip format error.'); 37 | 38 | ip=eps[0] 39 | eps2 = eps[1].split(' ') 40 | port=(int)(eps2[0]) 41 | group='' 42 | instance='' 43 | 44 | if(len(eps2)>1): 45 | group=eps2[1] 46 | if(len(eps2)>2): 47 | instance=eps2[2] 48 | 49 | servers.append({'server':ip,'port':port,'group':group,'instance':instance}) 50 | settings.save_settings(servers, sms) 51 | self.write({"status":200}) 52 | except Exception,ex: 53 | self.write({"status":500,"error":ex.message}) -------------------------------------------------------------------------------- /src/api/controller/SlowlogController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import datetime 3 | import redis 4 | 5 | class SlowlogController(BaseController): 6 | 7 | def get(self): 8 | data={} 9 | data['data']=[] 10 | server = self.get_argument("server").split(':') 11 | connection = redis.Redis(host=server[0], port=(int)(server[1]), db=0,socket_timeout=1) 12 | logs = connection.execute_command('slowlog','get','128') 13 | for lid,timeticks,run_micro,commands in logs: 14 | timestamp = datetime.datetime.fromtimestamp(int(timeticks)) 15 | cmd=' '.join(commands) 16 | data['data'].append({'id':lid,'time':str(timestamp),'escapeMs':(float)(run_micro)/1000,'cmd':cmd}) 17 | self.write(data) -------------------------------------------------------------------------------- /src/api/controller/StatusController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | class StatusController(BaseController): 8 | 9 | def get(self): 10 | return_data = {} 11 | return_data['data']=[] 12 | 13 | server = self.get_argument("server") 14 | from_date = self.get_argument("from", None) 15 | to_date = self.get_argument("to", None) 16 | 17 | if from_date == None or to_date == None or len(from_date) == 0: 18 | end = datetime.datetime.now() 19 | delta = datetime.timedelta(seconds=300) 20 | start = end - delta 21 | else: 22 | start = dateutil.parser.parse(from_date) 23 | end = dateutil.parser.parse(to_date) 24 | 25 | data = self.stats_provider.get_status_info(server, start, end) 26 | 27 | for item in data: 28 | row=item[1] 29 | timestamp = datetime.datetime.fromtimestamp(int(row['timestamp'])) 30 | row['time']= timestamp.strftime('%Y-%m-%d %H:%M:%S') 31 | return_data['data'].append(row) 32 | 33 | self.write(return_data) 34 | -------------------------------------------------------------------------------- /src/api/controller/TopCommandsController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class TopCommandsController(BaseController): 9 | 10 | def get(self): 11 | return_data = dict(data=[], 12 | timestamp=datetime.datetime.now().isoformat()) 13 | 14 | server = self.get_argument("server") 15 | from_date = self.get_argument("from", None) 16 | to_date = self.get_argument("to", None) 17 | 18 | if from_date==None or to_date==None: 19 | end = datetime.datetime.now() 20 | delta = datetime.timedelta(seconds=120) 21 | start = end - delta 22 | else: 23 | start = dateutil.parser.parse(from_date) 24 | end = dateutil.parser.parse(to_date) 25 | 26 | for data in self.stats_provider.get_top_commands_stats(server, start, 27 | end): 28 | return_data['data'].append([data[0], data[1]]) 29 | 30 | self.write(return_data) 31 | -------------------------------------------------------------------------------- /src/api/controller/TopKeysController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class TopKeysController(BaseController): 9 | 10 | def get(self): 11 | return_data = dict(data=[], timestamp=datetime.datetime.now().isoformat()) 12 | 13 | server = self.get_argument("server") 14 | from_date = self.get_argument("from", None) 15 | to_date = self.get_argument("to", None) 16 | 17 | if from_date==None or to_date==None: 18 | end = datetime.datetime.now() 19 | delta = datetime.timedelta(seconds=120) 20 | start = end - delta 21 | else: 22 | start = dateutil.parser.parse(from_date) 23 | end = dateutil.parser.parse(to_date) 24 | 25 | for data in self.stats_provider.get_top_keys_stats(server, start, end): 26 | return_data['data'].append([data[0], data[1]]) 27 | 28 | self.write(return_data) 29 | -------------------------------------------------------------------------------- /src/api/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/api/controller/__init__.py -------------------------------------------------------------------------------- /src/api/util/RDP.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Ramer-Douglas-Peucker algorithm roughly ported from the pseudo-code provided 3 | by http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm 4 | """ 5 | 6 | from math import sqrt 7 | 8 | def distance(a, b): 9 | return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) 10 | 11 | def point_line_distance(point, start, end): 12 | if (start == end): 13 | return distance(point, start) 14 | else: 15 | n = abs( 16 | (end[0] - start[0]) * (start[1] - point[1]) - (start[0] - point[0]) * (end[1] - start[1]) 17 | ) 18 | d = sqrt( 19 | (end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2 20 | ) 21 | return n / d 22 | 23 | def rdp(points, epsilon): 24 | """ 25 | Reduces a series of points to a simplified version that loses detail, but 26 | maintains the general shape of the series. 27 | """ 28 | dmax = 0.0 29 | index = 0 30 | for i in range(1, len(points) - 1): 31 | d = point_line_distance(points[i], points[0], points[-1]) 32 | if d > dmax: 33 | index = i 34 | dmax = d 35 | if dmax >= epsilon: 36 | results = rdp(points[:index+1], epsilon)[:-1] + rdp(points[index:], epsilon) 37 | else: 38 | results = [points[0], points[-1]] 39 | return results -------------------------------------------------------------------------------- /src/api/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/api/util/__init__.py -------------------------------------------------------------------------------- /src/api/util/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | def get_settings(): 5 | return json.load(open(os.path.abspath('.')+ "/redis_live.conf")) 6 | 7 | def get_redis_servers(): 8 | config = get_settings() 9 | servers= config["RedisServers"] 10 | data=[] 11 | for server in servers: 12 | server['ep']='%(server)s:%(port)d' % server 13 | if(server.get('group')==None or server.get('group')==''): 14 | server['group']='ungrouped' 15 | if(server.get('instance')==None or server.get('instance')==''): 16 | server['instance']=(str)(server['port']) 17 | data.append(server) 18 | return data 19 | 20 | def get_redis_alerturi(): 21 | config = get_settings() 22 | return config["sms_alert"] 23 | 24 | def get_redis_stats_server(): 25 | config = get_settings() 26 | return config["RedisStatsServer"] 27 | 28 | def get_data_store_type(): 29 | config = get_settings() 30 | return config["DataStoreType"] 31 | 32 | def get_master_slave_sms_type(): 33 | config = get_settings() 34 | return config['master_slave_sms'] 35 | 36 | def save_settings(redisServers,smsType): 37 | config = get_settings() 38 | config["RedisServers"]= redisServers; 39 | config['master_slave_sms']=smsType; 40 | 41 | data = json.dumps(config) 42 | data = data.replace('}', '}\r\n') 43 | output = open(os.path.abspath('.') + "/redis_live.conf", "w") 44 | output.truncate() 45 | output.write(data) 46 | output.close() 47 | -------------------------------------------------------------------------------- /src/daemonized.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | class daemonized(object): 5 | def __init__(self): 6 | pass 7 | 8 | def daemonize(self): 9 | pass 10 | 11 | try: 12 | pid = os.fork() 13 | if pid > 0: 14 | sys.exit(0) 15 | except OSError,e: 16 | sys.stderr.write("Fork 1 has failed --> %d--[%s]\n" \ 17 | % (e.errno,e.strerror)) 18 | sys.exit(1) 19 | 20 | #os.chdir('/') 21 | #detach from terminal 22 | os.setsid() 23 | #file to be created? 24 | os.umask(0) 25 | 26 | try: 27 | pid = os.fork() 28 | if pid > 0: 29 | print "Daemon process pid %d" % pid 30 | sys.exit(0) 31 | except OSError, e: 32 | sys.stderr.write("Fork 2 has failed --> %d--[%s]" \ 33 | % (e.errno, e.strerror)) 34 | sys.exit(1) 35 | 36 | sys.stdout.flush() 37 | sys.stderr.flush() 38 | if sys.platform != 'darwin': # This block breaks on OS X 39 | # Redirect standard file descriptors 40 | sys.stdout.flush() 41 | sys.stderr.flush() 42 | si = file( os.devnull, 'r') 43 | so = file( os.devnull, 'a+') 44 | se = file( os.devnull, 'a+', 0) 45 | 46 | os.dup2(si.fileno(), sys.stdin.fileno()) 47 | os.dup2(so.fileno(), sys.stdout.fileno()) 48 | os.dup2(se.fileno(), sys.stderr.fileno()) 49 | 50 | def start_daemon(self): 51 | self.daemonize() 52 | 53 | self.run_daemon() 54 | 55 | def start(self): 56 | self.run_daemon() 57 | 58 | def run_daemon(self): 59 | '''override''' 60 | pass 61 | 62 | -------------------------------------------------------------------------------- /src/dataprovider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/dataprovider/__init__.py -------------------------------------------------------------------------------- /src/dataprovider/dataprovider.py: -------------------------------------------------------------------------------- 1 | from api.util import settings 2 | import redisprovider 3 | 4 | 5 | # TODO: Confirm there's not some implementation detail I've missed, then 6 | # ditch the classes here. 7 | class RedisLiveDataProvider(object): 8 | 9 | @staticmethod 10 | def get_provider(): 11 | """Returns a data provider based on the settings file. 12 | 13 | Valid providers are currently Redis and SQLite. 14 | """ 15 | data_store_type = settings.get_data_store_type() 16 | 17 | # FIXME: Should use a global variable for "redis" here. 18 | if data_store_type == "redis": 19 | return redisprovider.RedisStatsProvider() -------------------------------------------------------------------------------- /src/dataprovider/redisprovider.py: -------------------------------------------------------------------------------- 1 | from api.util import settings 2 | from datetime import datetime, timedelta 3 | import redis 4 | import json 5 | import ast 6 | import time 7 | import struct 8 | 9 | def datetime2_unix_int(timestamp): 10 | return (int)(time.mktime(timestamp.timetuple())) 11 | 12 | class RedisStatsProvider(object): 13 | def __init__(self): 14 | stats_server = settings.get_redis_stats_server() 15 | self.server = stats_server["server"] 16 | self.port = stats_server["port"] 17 | self.password = stats_server["password"] 18 | self.conn = redis.StrictRedis(host=self.server, port=self.port, db=0, password=self.password) 19 | 20 | def save_keys_Info(self, server,rediskey,timestamp, expires, persists,expired,evicted 21 | ,hit_rate,commands,used,peak): 22 | score=datetime2_unix_int(timestamp) 23 | data=struct.pack('iiiiiiiqq', 24 | score, 25 | commands, 26 | expires, 27 | persists, 28 | expired, 29 | evicted, 30 | hit_rate, 31 | peak, 32 | used) 33 | self.conn.zadd(server +':'+ rediskey, score, data) 34 | 35 | def get_keys_info(self, server, from_date, to_date): 36 | data = [] 37 | start = datetime2_unix_int(from_date) 38 | end = datetime2_unix_int(to_date) 39 | key=server + ":info" 40 | if(end-start>=3600*2): 41 | key=key+"_hours" 42 | rows = self.conn.zrangebyscore(key, start, end) 43 | 44 | rate=1 45 | if(len(rows)> 400): 46 | rate=len(rows)/300 47 | 48 | index=0 49 | for row in rows: 50 | index+=1 51 | if(index%rate==0): 52 | row=struct.unpack('iiiiiiiqq',row) 53 | timestamp = datetime.fromtimestamp(int(row[0])) 54 | item=list(row) 55 | item[0]=tuple(timestamp.timetuple())[:-2] 56 | 57 | data.append(item) 58 | return data 59 | 60 | def save_status_info(self, server, timestamp, data): 61 | timestamp=datetime2_unix_int(timestamp) 62 | data['timestamp']=timestamp 63 | self.conn.zadd(server + ":status", timestamp, json.dumps(data)) 64 | 65 | def get_status_info(self, server, from_date, to_date): 66 | data = [] 67 | start = datetime2_unix_int(from_date) 68 | end = datetime2_unix_int(to_date) 69 | rows = self.conn.zrangebyscore(server + ":status", start, end) 70 | for row in rows: 71 | row = ast.literal_eval(row) 72 | timestamp = datetime.fromtimestamp(int(row['timestamp'])) 73 | data.append([tuple(timestamp.timetuple())[:-2], row]) 74 | return data 75 | 76 | def delete_history(self,server,timestamp): 77 | begin=0 78 | end = datetime2_unix_int(timestamp) 79 | self.conn.zremrangebyscore(server + ":info", begin, end) 80 | self.conn.zremrangebyscore(server + ":info_hours", begin, end-(3600*24*7)) 81 | # status for more then 3 month 82 | self.conn.zremrangebyscore(server + ":status", begin, end - (3600*24*90)) 83 | 84 | def collection_database(self): 85 | self.conn.bgrewriteaof() 86 | -------------------------------------------------------------------------------- /src/redis_live.conf: -------------------------------------------------------------------------------- 1 | { 2 | "master_slave_sms": "1,1", 3 | "RedisStatsServer": { 4 | "port": 6379, 5 | "server": "127.0.0.1", 6 | "password": "XXXXXXXXX" 7 | }, 8 | "sms_alert": "192.168.110.207:9999", 9 | "DataStoreType": "redis", 10 | "RedisServers": [{ 11 | "instance": "Master1", 12 | "group": "Test1", 13 | "port": 6379, 14 | "server": "127.0.0.1" 15 | }, { 16 | "instance": "Slave1", 17 | "group": "Test1", 18 | "port": 6380, 19 | "server": "127.0.0.1", 20 | "password": "XXXXXXXXX" 21 | }] 22 | } 23 | -------------------------------------------------------------------------------- /src/redis_live.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os 3 | import tornado.ioloop 4 | import tornado.options 5 | from tornado.options import define, options 6 | import tornado.web 7 | 8 | from api.controller.BaseStaticFileHandler import BaseStaticFileHandler 9 | 10 | from api.controller.ServerListController import ServerListController 11 | from api.controller.InfoController import InfoController 12 | from api.controller.CommandsController import CommandsController 13 | from api.controller.InfoListController import InfoListController 14 | from api.controller.StatusController import StatusController 15 | from api.controller.SettingsController import SettingsController 16 | from api.controller.SlowlogController import SlowlogController 17 | from daemonized import daemonized 18 | 19 | class redis_live(daemonized): 20 | def run_daemon(self): 21 | 22 | define("port", default=8888, help="run on the given port", type=int) 23 | define("debug", default=0, help="debug mode", type=int) 24 | tornado.options.parse_command_line() 25 | 26 | print os.path.abspath('.') 27 | # Bootup 28 | handlers = [ 29 | (r"/api/servers", ServerListController), 30 | (r"/api/info", InfoController), 31 | (r"/api/status", StatusController), 32 | (r"/api/infolist",InfoListController), 33 | (r"/api/commands", CommandsController), 34 | (r"/api/settings",SettingsController), 35 | (r"/api/slowlog",SlowlogController), 36 | (r"/(.*)", BaseStaticFileHandler, {"path": os.path.abspath('.')+'/www'}) 37 | ] 38 | 39 | server_settings = {'debug': options.debug} 40 | application = tornado.web.Application(handlers, **server_settings) 41 | application.listen(options.port) 42 | print("start at:0.0.0.0:%d http://127.0.0.1:8888/index.html" %(options.port)) 43 | tornado.ioloop.IOLoop.instance().start() 44 | 45 | if __name__ == "__main__": 46 | live= redis_live() 47 | live.start() 48 | -------------------------------------------------------------------------------- /src/redis_live_daemon.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | curpath = os.path.split(os.path.realpath(sys.argv[0]))[0] 7 | os.chdir(curpath) 8 | 9 | from redis_live import redis_live 10 | live = redis_live() 11 | live.start_daemon() 12 | -------------------------------------------------------------------------------- /src/redis_monitor_daemon.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | curpath = os.path.split(os.path.realpath(sys.argv[0]))[0] 7 | os.chdir(curpath) 8 | 9 | from redis_monitor import redis_monitor 10 | montor = redis_monitor() 11 | montor.start_daemon() 12 | -------------------------------------------------------------------------------- /src/www/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/www/images/logo.png -------------------------------------------------------------------------------- /src/www/js/app.js: -------------------------------------------------------------------------------- 1 | /* Main App 2 | * ====================== */ 3 | 4 | var App = { 5 | 6 | init: function() { 7 | 8 | this.RegisterPartials() 9 | this.RegisterHelpers() 10 | 11 | var ServerDropDown = new ServerList({ 12 | el : $("#server-list") 13 | , model : new ServerListModel() 14 | }) 15 | 16 | var infoWidget = new InfoWidget({ 17 | el : $("#info-widget-placeholder") 18 | , model : new InfoWidgetModel() 19 | }) 20 | 21 | var commandsWidget = new CommandsWidget({ 22 | el : $("#commands-widget-placeholder") 23 | , model : new CommandsWidgetModel() 24 | }) 25 | var statusWidget = new StatusWidget({ 26 | el : $("#status-widget-placeholder") 27 | , model : new StatusWidgetModel() 28 | }) 29 | 30 | } 31 | 32 | , RegisterPartials : function(){ 33 | 34 | // Handlebars.registerPartial("date-dropdown", $("#date-dropdown-template").html()); 35 | 36 | } 37 | 38 | , RegisterHelpers : function(){ 39 | 40 | Handlebars.registerHelper('hash', function ( context, options ) { 41 | 42 | var ret = "" 43 | , counter = 0 44 | 45 | $.each(context, function ( key, value ) { 46 | 47 | if (typeof value != "object") { 48 | obj = { "key" : key, "value" : value , "index" : counter++ } 49 | ret = ret + options.fn(obj) 50 | } 51 | 52 | }) 53 | 54 | return ret 55 | }) 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/www/js/libs/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittlePeng/redis-monitor/e61648c462e3f5534de612ad382bc70c74975180/src/www/js/libs/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/README.md: -------------------------------------------------------------------------------- 1 | ## 2.0 BOOTSTRAP JS PHILOSOPHY 2 | These are the high-level design rules which guide the development of Bootstrap's plugin apis. 3 | 4 | --- 5 | 6 | ### DATA-ATTRIBUTE API 7 | 8 | We believe you should be able to use all plugins provided by Bootstrap purely through the markup API without writing a single line of javascript. 9 | 10 | We acknowledge that this isn't always the most performant and sometimes it may be desirable to turn this functionality off altogether. Therefore, as of 2.0 we provide the ability to disable the data attribute API by unbinding all events on the body namespaced with `'data-api'`. This looks like this: 11 | 12 | $('body').off('.data-api') 13 | 14 | To target a specific plugin, just include the plugins name as a namespace along with the data-api namespace like this: 15 | 16 | $('body').off('.alert.data-api') 17 | 18 | --- 19 | 20 | ### PROGRAMATIC API 21 | 22 | We also believe you should be able to use all plugins provided by Bootstrap purely through the JS API. 23 | 24 | All public APIs should be single, chainable methods, and return the collection acted upon. 25 | 26 | $(".btn.danger").button("toggle").addClass("fat") 27 | 28 | All methods should accept an optional options object, a string which targets a particular method, or null which initiates the default behavior: 29 | 30 | $("#myModal").modal() // initialized with defaults 31 | $("#myModal").modal({ keyboard: false }) // initialized with no keyboard 32 | $("#myModal").modal('show') // initializes and invokes show immediately afterqwe2 33 | 34 | --- 35 | 36 | ### OPTIONS 37 | 38 | Options should be sparse and add universal value. We should pick the right defaults. 39 | 40 | All plugins should have a default object which can be modified to affect all instances' default options. The defaults object should be available via `$.fn.plugin.defaults`. 41 | 42 | $.fn.modal.defaults = { … } 43 | 44 | An options definition should take the following form: 45 | 46 | *noun*: *adjective* - describes or modifies a quality of an instance 47 | 48 | examples: 49 | 50 | backdrop: true 51 | keyboard: false 52 | placement: 'top' 53 | 54 | --- 55 | 56 | ### EVENTS 57 | 58 | All events should have an infinitive and past participle form. The infinitive is fired just before an action takes place, the past participle on completion of the action. 59 | 60 | show | shown 61 | hide | hidden 62 | 63 | --- 64 | 65 | ### CONSTRUCTORS 66 | 67 | Each plugin should expose its raw constructor on a `Constructor` property -- accessed in the following way: 68 | 69 | 70 | $.fn.popover.Constructor 71 | 72 | --- 73 | 74 | ### DATA ACCESSOR 75 | 76 | Each plugin stores a copy of the invoked class on an object. This class instance can be accessed directly through jQuery's data API like this: 77 | 78 | $('[rel=popover]').data('popover') instanceof $.fn.popover.Constructor 79 | 80 | --- 81 | 82 | ### DATA ATTRIBUTES 83 | 84 | Data attributes should take the following form: 85 | 86 | - data-{{verb}}={{plugin}} - defines main interaction 87 | - data-target || href^=# - defined on "control" element (if element controls an element other than self) 88 | - data-{{noun}} - defines class instance options 89 | 90 | examples: 91 | 92 | // control other targets 93 | data-toggle="modal" data-target="#foo" 94 | data-toggle="collapse" data-target="#foo" data-parent="#bar" 95 | 96 | // defined on element they control 97 | data-spy="scroll" 98 | 99 | data-dismiss="modal" 100 | data-dismiss="alert" 101 | 102 | data-toggle="dropdown" 103 | 104 | data-toggle="button" 105 | data-toggle="buttons-checkbox" 106 | data-toggle="buttons-radio" -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-alert.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-alert.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#alerts 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* ALERT CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var dismiss = '[data-dismiss="alert"]' 29 | , Alert = function ( el ) { 30 | $(el).on('click', dismiss, this.close) 31 | } 32 | 33 | Alert.prototype = { 34 | 35 | constructor: Alert 36 | 37 | , close: function ( e ) { 38 | var $this = $(this) 39 | , selector = $this.attr('data-target') 40 | , $parent 41 | 42 | if (!selector) { 43 | selector = $this.attr('href') 44 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 45 | } 46 | 47 | $parent = $(selector) 48 | $parent.trigger('close') 49 | 50 | e && e.preventDefault() 51 | 52 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 53 | 54 | $parent 55 | .trigger('close') 56 | .removeClass('in') 57 | 58 | function removeElement() { 59 | $parent 60 | .trigger('closed') 61 | .remove() 62 | } 63 | 64 | $.support.transition && $parent.hasClass('fade') ? 65 | $parent.on($.support.transition.end, removeElement) : 66 | removeElement() 67 | } 68 | 69 | } 70 | 71 | 72 | /* ALERT PLUGIN DEFINITION 73 | * ======================= */ 74 | 75 | $.fn.alert = function ( option ) { 76 | return this.each(function () { 77 | var $this = $(this) 78 | , data = $this.data('alert') 79 | if (!data) $this.data('alert', (data = new Alert(this))) 80 | if (typeof option == 'string') data[option].call($this) 81 | }) 82 | } 83 | 84 | $.fn.alert.Constructor = Alert 85 | 86 | 87 | /* ALERT DATA-API 88 | * ============== */ 89 | 90 | $(function () { 91 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) 92 | }) 93 | 94 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-button.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-button.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#buttons 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | !function( $ ){ 21 | 22 | "use strict" 23 | 24 | /* BUTTON PUBLIC CLASS DEFINITION 25 | * ============================== */ 26 | 27 | var Button = function ( element, options ) { 28 | this.$element = $(element) 29 | this.options = $.extend({}, $.fn.button.defaults, options) 30 | } 31 | 32 | Button.prototype = { 33 | 34 | constructor: Button 35 | 36 | , setState: function ( state ) { 37 | var d = 'disabled' 38 | , $el = this.$element 39 | , data = $el.data() 40 | , val = $el.is('input') ? 'val' : 'html' 41 | 42 | state = state + 'Text' 43 | data.resetText || $el.data('resetText', $el[val]()) 44 | 45 | $el[val](data[state] || this.options[state]) 46 | 47 | // push to event loop to allow forms to submit 48 | setTimeout(function () { 49 | state == 'loadingText' ? 50 | $el.addClass(d).attr(d, d) : 51 | $el.removeClass(d).removeAttr(d) 52 | }, 0) 53 | } 54 | 55 | , toggle: function () { 56 | var $parent = this.$element.parent('[data-toggle="buttons-radio"]') 57 | 58 | $parent && $parent 59 | .find('.active') 60 | .removeClass('active') 61 | 62 | this.$element.toggleClass('active') 63 | } 64 | 65 | } 66 | 67 | 68 | /* BUTTON PLUGIN DEFINITION 69 | * ======================== */ 70 | 71 | $.fn.button = function ( option ) { 72 | return this.each(function () { 73 | var $this = $(this) 74 | , data = $this.data('button') 75 | , options = typeof option == 'object' && option 76 | if (!data) $this.data('button', (data = new Button(this, options))) 77 | if (option == 'toggle') data.toggle() 78 | else if (option) data.setState(option) 79 | }) 80 | } 81 | 82 | $.fn.button.defaults = { 83 | loadingText: 'loading...' 84 | } 85 | 86 | $.fn.button.Constructor = Button 87 | 88 | 89 | /* BUTTON DATA-API 90 | * =============== */ 91 | 92 | $(function () { 93 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { 94 | var $btn = $(e.target) 95 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 96 | $btn.button('toggle') 97 | }) 98 | }) 99 | 100 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-carousel.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-carousel.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#carousel 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* CAROUSEL CLASS DEFINITION 26 | * ========================= */ 27 | 28 | var Carousel = function (element, options) { 29 | this.$element = $(element) 30 | this.options = $.extend({}, $.fn.carousel.defaults, options) 31 | this.options.slide && this.slide(this.options.slide) 32 | this.options.pause == 'hover' && this.$element 33 | .on('mouseenter', $.proxy(this.pause, this)) 34 | .on('mouseleave', $.proxy(this.cycle, this)) 35 | } 36 | 37 | Carousel.prototype = { 38 | 39 | cycle: function () { 40 | this.interval = setInterval($.proxy(this.next, this), this.options.interval) 41 | return this 42 | } 43 | 44 | , to: function (pos) { 45 | var $active = this.$element.find('.active') 46 | , children = $active.parent().children() 47 | , activePos = children.index($active) 48 | , that = this 49 | 50 | if (pos > (children.length - 1) || pos < 0) return 51 | 52 | if (this.sliding) { 53 | return this.$element.one('slid', function () { 54 | that.to(pos) 55 | }) 56 | } 57 | 58 | if (activePos == pos) { 59 | return this.pause().cycle() 60 | } 61 | 62 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) 63 | } 64 | 65 | , pause: function () { 66 | clearInterval(this.interval) 67 | this.interval = null 68 | return this 69 | } 70 | 71 | , next: function () { 72 | if (this.sliding) return 73 | return this.slide('next') 74 | } 75 | 76 | , prev: function () { 77 | if (this.sliding) return 78 | return this.slide('prev') 79 | } 80 | 81 | , slide: function (type, next) { 82 | var $active = this.$element.find('.active') 83 | , $next = next || $active[type]() 84 | , isCycling = this.interval 85 | , direction = type == 'next' ? 'left' : 'right' 86 | , fallback = type == 'next' ? 'first' : 'last' 87 | , that = this 88 | 89 | this.sliding = true 90 | 91 | isCycling && this.pause() 92 | 93 | $next = $next.length ? $next : this.$element.find('.item')[fallback]() 94 | 95 | if ($next.hasClass('active')) return 96 | 97 | if (!$.support.transition && this.$element.hasClass('slide')) { 98 | this.$element.trigger('slide') 99 | $active.removeClass('active') 100 | $next.addClass('active') 101 | this.sliding = false 102 | this.$element.trigger('slid') 103 | } else { 104 | $next.addClass(type) 105 | $next[0].offsetWidth // force reflow 106 | $active.addClass(direction) 107 | $next.addClass(direction) 108 | this.$element.trigger('slide') 109 | this.$element.one($.support.transition.end, function () { 110 | $next.removeClass([type, direction].join(' ')).addClass('active') 111 | $active.removeClass(['active', direction].join(' ')) 112 | that.sliding = false 113 | setTimeout(function () { that.$element.trigger('slid') }, 0) 114 | }) 115 | } 116 | 117 | isCycling && this.cycle() 118 | 119 | return this 120 | } 121 | 122 | } 123 | 124 | 125 | /* CAROUSEL PLUGIN DEFINITION 126 | * ========================== */ 127 | 128 | $.fn.carousel = function ( option ) { 129 | return this.each(function () { 130 | var $this = $(this) 131 | , data = $this.data('carousel') 132 | , options = typeof option == 'object' && option 133 | if (!data) $this.data('carousel', (data = new Carousel(this, options))) 134 | if (typeof option == 'number') data.to(option) 135 | else if (typeof option == 'string' || (option = options.slide)) data[option]() 136 | else data.cycle() 137 | }) 138 | } 139 | 140 | $.fn.carousel.defaults = { 141 | interval: 5000 142 | , pause: 'hover' 143 | } 144 | 145 | $.fn.carousel.Constructor = Carousel 146 | 147 | 148 | /* CAROUSEL DATA-API 149 | * ================= */ 150 | 151 | $(function () { 152 | $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { 153 | var $this = $(this), href 154 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 155 | , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) 156 | $target.carousel(options) 157 | e.preventDefault() 158 | }) 159 | }) 160 | 161 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-collapse.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-collapse.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#collapse 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | !function( $ ){ 21 | 22 | "use strict" 23 | 24 | var Collapse = function ( element, options ) { 25 | this.$element = $(element) 26 | this.options = $.extend({}, $.fn.collapse.defaults, options) 27 | 28 | if (this.options["parent"]) { 29 | this.$parent = $(this.options["parent"]) 30 | } 31 | 32 | this.options.toggle && this.toggle() 33 | } 34 | 35 | Collapse.prototype = { 36 | 37 | constructor: Collapse 38 | 39 | , dimension: function () { 40 | var hasWidth = this.$element.hasClass('width') 41 | return hasWidth ? 'width' : 'height' 42 | } 43 | 44 | , show: function () { 45 | var dimension = this.dimension() 46 | , scroll = $.camelCase(['scroll', dimension].join('-')) 47 | , actives = this.$parent && this.$parent.find('.in') 48 | , hasData 49 | 50 | if (actives && actives.length) { 51 | hasData = actives.data('collapse') 52 | actives.collapse('hide') 53 | hasData || actives.data('collapse', null) 54 | } 55 | 56 | this.$element[dimension](0) 57 | this.transition('addClass', 'show', 'shown') 58 | this.$element[dimension](this.$element[0][scroll]) 59 | 60 | } 61 | 62 | , hide: function () { 63 | var dimension = this.dimension() 64 | this.reset(this.$element[dimension]()) 65 | this.transition('removeClass', 'hide', 'hidden') 66 | this.$element[dimension](0) 67 | } 68 | 69 | , reset: function ( size ) { 70 | var dimension = this.dimension() 71 | 72 | this.$element 73 | .removeClass('collapse') 74 | [dimension](size || 'auto') 75 | [0].offsetWidth 76 | 77 | this.$element[size ? 'addClass' : 'removeClass']('collapse') 78 | 79 | return this 80 | } 81 | 82 | , transition: function ( method, startEvent, completeEvent ) { 83 | var that = this 84 | , complete = function () { 85 | if (startEvent == 'show') that.reset() 86 | that.$element.trigger(completeEvent) 87 | } 88 | 89 | this.$element 90 | .trigger(startEvent) 91 | [method]('in') 92 | 93 | $.support.transition && this.$element.hasClass('collapse') ? 94 | this.$element.one($.support.transition.end, complete) : 95 | complete() 96 | } 97 | 98 | , toggle: function () { 99 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 100 | } 101 | 102 | } 103 | 104 | /* COLLAPSIBLE PLUGIN DEFINITION 105 | * ============================== */ 106 | 107 | $.fn.collapse = function ( option ) { 108 | return this.each(function () { 109 | var $this = $(this) 110 | , data = $this.data('collapse') 111 | , options = typeof option == 'object' && option 112 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 113 | if (typeof option == 'string') data[option]() 114 | }) 115 | } 116 | 117 | $.fn.collapse.defaults = { 118 | toggle: true 119 | } 120 | 121 | $.fn.collapse.Constructor = Collapse 122 | 123 | 124 | /* COLLAPSIBLE DATA-API 125 | * ==================== */ 126 | 127 | $(function () { 128 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { 129 | var $this = $(this), href 130 | , target = $this.attr('data-target') 131 | || e.preventDefault() 132 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 133 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 134 | $(target).collapse(option) 135 | }) 136 | }) 137 | 138 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-dropdown.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-dropdown.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* DROPDOWN CLASS DEFINITION 26 | * ========================= */ 27 | 28 | var toggle = '[data-toggle="dropdown"]' 29 | , Dropdown = function ( element ) { 30 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 31 | $('html').on('click.dropdown.data-api', function () { 32 | $el.parent().removeClass('open') 33 | }) 34 | } 35 | 36 | Dropdown.prototype = { 37 | 38 | constructor: Dropdown 39 | 40 | , toggle: function ( e ) { 41 | var $this = $(this) 42 | , selector = $this.attr('data-target') 43 | , $parent 44 | , isActive 45 | 46 | if (!selector) { 47 | selector = $this.attr('href') 48 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 49 | } 50 | 51 | $parent = $(selector) 52 | $parent.length || ($parent = $this.parent()) 53 | 54 | isActive = $parent.hasClass('open') 55 | 56 | clearMenus() 57 | !isActive && $parent.toggleClass('open') 58 | 59 | return false 60 | } 61 | 62 | } 63 | 64 | function clearMenus() { 65 | $(toggle).parent().removeClass('open') 66 | } 67 | 68 | 69 | /* DROPDOWN PLUGIN DEFINITION 70 | * ========================== */ 71 | 72 | $.fn.dropdown = function ( option ) { 73 | return this.each(function () { 74 | var $this = $(this) 75 | , data = $this.data('dropdown') 76 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 77 | if (typeof option == 'string') data[option].call($this) 78 | }) 79 | } 80 | 81 | $.fn.dropdown.Constructor = Dropdown 82 | 83 | 84 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 85 | * =================================== */ 86 | 87 | $(function () { 88 | $('html').on('click.dropdown.data-api', clearMenus) 89 | $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) 90 | }) 91 | 92 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#modals 4 | * ========================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* MODAL CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var Modal = function ( content, options ) { 29 | this.options = options 30 | this.$element = $(content) 31 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 32 | } 33 | 34 | Modal.prototype = { 35 | 36 | constructor: Modal 37 | 38 | , toggle: function () { 39 | return this[!this.isShown ? 'show' : 'hide']() 40 | } 41 | 42 | , show: function () { 43 | var that = this 44 | 45 | if (this.isShown) return 46 | 47 | $('body').addClass('modal-open') 48 | 49 | this.isShown = true 50 | this.$element.trigger('show') 51 | 52 | escape.call(this) 53 | backdrop.call(this, function () { 54 | var transition = $.support.transition && that.$element.hasClass('fade') 55 | 56 | !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position 57 | 58 | that.$element 59 | .show() 60 | 61 | if (transition) { 62 | that.$element[0].offsetWidth // force reflow 63 | } 64 | 65 | that.$element.addClass('in') 66 | 67 | transition ? 68 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 69 | that.$element.trigger('shown') 70 | 71 | }) 72 | } 73 | 74 | , hide: function ( e ) { 75 | e && e.preventDefault() 76 | 77 | if (!this.isShown) return 78 | 79 | var that = this 80 | this.isShown = false 81 | 82 | $('body').removeClass('modal-open') 83 | 84 | escape.call(this) 85 | 86 | this.$element 87 | .trigger('hide') 88 | .removeClass('in') 89 | 90 | $.support.transition && this.$element.hasClass('fade') ? 91 | hideWithTransition.call(this) : 92 | hideModal.call(this) 93 | } 94 | 95 | } 96 | 97 | 98 | /* MODAL PRIVATE METHODS 99 | * ===================== */ 100 | 101 | function hideWithTransition() { 102 | var that = this 103 | , timeout = setTimeout(function () { 104 | that.$element.off($.support.transition.end) 105 | hideModal.call(that) 106 | }, 500) 107 | 108 | this.$element.one($.support.transition.end, function () { 109 | clearTimeout(timeout) 110 | hideModal.call(that) 111 | }) 112 | } 113 | 114 | function hideModal( that ) { 115 | this.$element 116 | .hide() 117 | .trigger('hidden') 118 | 119 | backdrop.call(this) 120 | } 121 | 122 | function backdrop( callback ) { 123 | var that = this 124 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 125 | 126 | if (this.isShown && this.options.backdrop) { 127 | var doAnimate = $.support.transition && animate 128 | 129 | this.$backdrop = $('