redmon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +1 -0
- data/Gemfile +3 -0
- data/README.md +71 -0
- data/Rakefile +11 -0
- data/bin/redmon +71 -0
- data/lib/redmon.rb +67 -0
- data/lib/redmon/app.rb +44 -0
- data/lib/redmon/helpers.rb +18 -0
- data/lib/redmon/public/redmon.css +114 -0
- data/lib/redmon/public/redmon.js +556 -0
- data/lib/redmon/public/vendor/bootstrap-modal.js +260 -0
- data/lib/redmon/public/vendor/bootstrap.min.css +356 -0
- data/lib/redmon/public/vendor/jquery-1.7.1.min.js +4 -0
- data/lib/redmon/public/vendor/jquery-effects-core.min.js +20 -0
- data/lib/redmon/public/vendor/jquery.editinplace.js +646 -0
- data/lib/redmon/public/vendor/jquery.flot.js +2651 -0
- data/lib/redmon/public/vendor/terminal.js +135 -0
- data/lib/redmon/redis.rb +63 -0
- data/lib/redmon/version.rb +3 -0
- data/lib/redmon/views/app.haml +178 -0
- data/lib/redmon/views/cli.haml +15 -0
- data/lib/redmon/worker.rb +41 -0
- data/load_sim.rb +20 -0
- data/redmon.gemspec +38 -0
- data/spec/app_spec.rb +95 -0
- data/spec/helpers_spec.rb +86 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/worker_spec.rb +62 -0
- metadata +262 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm: 1.9.3
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Redmon
|
2
|
+
|
3
|
+
** Work in progress in the very early stages of dev **
|
4
|
+
|
5
|
+
Simple sinatra based dashbord for redis. After seeing the [fnordmetric](https://github.com/paulasmuth/fnordmetric)
|
6
|
+
project I was inspired to write this. Some of the ideas there have be carried over here.
|
7
|
+
|
8
|
+
[ ![Build status - Travis-ci](https://secure.travis-ci.org/steelThread/redmon.png) ](http://travis-ci.org/steelThread/redmon)
|
9
|
+
|
10
|
+
----
|
11
|
+
|
12
|
+
Watch your redis server live.
|
13
|
+
|
14
|
+
![](http://dl.dropbox.com/u/27525257/dash-new.png)
|
15
|
+
|
16
|
+
----
|
17
|
+
|
18
|
+
Interact with redis using a familiar cli interface.
|
19
|
+
|
20
|
+
![](http://dl.dropbox.com/u/27525257/cli.png)
|
21
|
+
|
22
|
+
----
|
23
|
+
|
24
|
+
Dynamically update your server configuration.
|
25
|
+
|
26
|
+
![](http://dl.dropbox.com/u/27525257/configuration-new.png)
|
27
|
+
|
28
|
+
----
|
29
|
+
|
30
|
+
Intuitively introspect registered keys. ** Coming Soon **
|
31
|
+
|
32
|
+
----
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
Currently not a registered gem, but soon. For now clone the repo and start the app as demonstrated below
|
36
|
+
|
37
|
+
```bash
|
38
|
+
$ bundle install
|
39
|
+
$ bundle exec bin/redmon -h
|
40
|
+
Usage: bin/redmon (options)
|
41
|
+
-a, --address ADDRESS The thin bind address for the app (default: 0.0.0.0)
|
42
|
+
-n, --namespace NAMESPACE The root Redis namespace (default: redmon)
|
43
|
+
-i, --interval SECS Poll interval in secs for the worker (default: 10)
|
44
|
+
-p, --port PORT The thin bind port for the app (default: 4567)
|
45
|
+
-r, --redis URL The Redis url for monitor (default: redis://127.0.0.1:6379)
|
46
|
+
--no-app Do not run the web app to present stats
|
47
|
+
--no-worker Do not run a worker to collect the stats
|
48
|
+
$ bundle exec bin/redmon
|
49
|
+
>> Thin web server (v1.3.1 codename Triple Espresso)
|
50
|
+
>> Maximum connections set to 1024
|
51
|
+
>> Listening on 0.0.0.0:4567, CTRL+C to stop
|
52
|
+
[12-03-10 15:49:40] listening on http#0.0.0.0:4567
|
53
|
+
```
|
54
|
+
|
55
|
+
If you want to simulate a weak load on redis
|
56
|
+
|
57
|
+
```bash
|
58
|
+
$ ruby load_sim.rb
|
59
|
+
```
|
60
|
+
|
61
|
+
Open your browser to 0.0.0.0:4567
|
62
|
+
|
63
|
+
## License
|
64
|
+
|
65
|
+
Copyright (c) 2012 Sean McDaniel
|
66
|
+
|
67
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy and modify copies of the Software, subject to the following conditions:
|
68
|
+
|
69
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
70
|
+
|
71
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/bin/redmon
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'mixlib/cli'
|
4
|
+
require 'redmon'
|
5
|
+
|
6
|
+
class RedmonCLI
|
7
|
+
include Mixlib::CLI
|
8
|
+
|
9
|
+
to_i =-> s {s.to_i}
|
10
|
+
|
11
|
+
option :address,
|
12
|
+
:short => '-a ADDRESS',
|
13
|
+
:long => '--address ADDRESS',
|
14
|
+
:default => '0.0.0.0',
|
15
|
+
:description => "The thin bind address for the app (default: 0.0.0.0)"
|
16
|
+
|
17
|
+
option :port,
|
18
|
+
:short => '-p PORT',
|
19
|
+
:long => '--port PORT',
|
20
|
+
:default => 4567,
|
21
|
+
:description => "The thin bind port for the app (default: 4567)",
|
22
|
+
:proc => to_i
|
23
|
+
|
24
|
+
option :redis_url,
|
25
|
+
:short => '-r URL',
|
26
|
+
:long => '--redis URL',
|
27
|
+
:default => 'redis://127.0.0.1:6379',
|
28
|
+
:description => "The Redis url for monitor (default: redis://127.0.0.1:6379)"
|
29
|
+
|
30
|
+
option :namespace,
|
31
|
+
:short => '-n NAMESPACE',
|
32
|
+
:long => '--namespace NAMESPACE',
|
33
|
+
:default => 'redmon',
|
34
|
+
:description => 'The root Redis namespace (default: redmon)'
|
35
|
+
|
36
|
+
option :poll_interval,
|
37
|
+
:short => '-i SECS',
|
38
|
+
:long => '--interval SECS',
|
39
|
+
:default => 10,
|
40
|
+
:description => 'Poll interval in secs for the worker (default: 10)',
|
41
|
+
:proc => to_i
|
42
|
+
|
43
|
+
option :app,
|
44
|
+
:on => :tail,
|
45
|
+
:long => '--no-app',
|
46
|
+
:boolean => true,
|
47
|
+
:default => true,
|
48
|
+
:description => 'Do not run the web app to present stats'
|
49
|
+
|
50
|
+
option :worker,
|
51
|
+
:on => :tail,
|
52
|
+
:long => '--no-worker',
|
53
|
+
:boolean => true,
|
54
|
+
:default => true,
|
55
|
+
:description => 'Do not run a worker to collect the stats'
|
56
|
+
|
57
|
+
def parse
|
58
|
+
parse_options
|
59
|
+
config[:web_interface] = web_interface
|
60
|
+
config
|
61
|
+
end
|
62
|
+
|
63
|
+
def web_interface
|
64
|
+
if config[:app]
|
65
|
+
config[:web_interface] = [config[:address], config[:port]]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
Redmon.run RedmonCLI.new.parse
|
data/lib/redmon.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'haml'
|
4
|
+
require 'redis'
|
5
|
+
require 'sinatra/base'
|
6
|
+
require 'thin'
|
7
|
+
|
8
|
+
module Redmon
|
9
|
+
extend self
|
10
|
+
|
11
|
+
attr_reader :opts
|
12
|
+
|
13
|
+
@opts = {
|
14
|
+
:web_interface => ['0.0.0.0', 4567],
|
15
|
+
:redis_url => 'redis://127.0.0.1:6379',
|
16
|
+
:namespace => 'redmon',
|
17
|
+
:worker => true,
|
18
|
+
:poll_interval => 10
|
19
|
+
}
|
20
|
+
|
21
|
+
def run(opts={})
|
22
|
+
@opts.merge! opts
|
23
|
+
start_em
|
24
|
+
rescue Exception => e
|
25
|
+
log "!!! Redmon has shit the bed, restarting... #{e.message}"
|
26
|
+
sleep(1); run(opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_em
|
30
|
+
EM.run do
|
31
|
+
trap 'TERM', &method(:shutdown)
|
32
|
+
trap 'INT', &method(:shutdown)
|
33
|
+
start_app if opts[:web_interface]
|
34
|
+
start_worker if opts[:worker]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def start_app
|
39
|
+
app = Redmon::App.new
|
40
|
+
Thin::Server.start(*opts[:web_interface], app)
|
41
|
+
log "listening on http##{opts[:web_interface].join(':')}"
|
42
|
+
rescue Exception => e
|
43
|
+
log "Can't start Redmon::App. port in use? Error #{e}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def start_worker
|
47
|
+
Worker.new.run!
|
48
|
+
end
|
49
|
+
|
50
|
+
def shutdown
|
51
|
+
EM.stop
|
52
|
+
end
|
53
|
+
|
54
|
+
def log(msg)
|
55
|
+
puts "[#{Time.now.strftime('%y-%m-%d %H:%M:%S')}] #{msg}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def [](option)
|
59
|
+
opts[option]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'redmon/redis'
|
65
|
+
require 'redmon/helpers'
|
66
|
+
require 'redmon/app'
|
67
|
+
require 'redmon/worker'
|
data/lib/redmon/app.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Redmon
|
2
|
+
class App < Sinatra::Base
|
3
|
+
|
4
|
+
configure :development do
|
5
|
+
require "sinatra/reloader"
|
6
|
+
register Sinatra::Reloader
|
7
|
+
end
|
8
|
+
|
9
|
+
helpers Redmon::Helpers
|
10
|
+
|
11
|
+
get '/' do
|
12
|
+
haml :app
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/cli' do
|
16
|
+
args = params[:command].split
|
17
|
+
@cmd = args.shift.downcase.intern
|
18
|
+
begin
|
19
|
+
raise RuntimeError unless supported? @cmd
|
20
|
+
@result = redis.send @cmd, *args
|
21
|
+
@result = empty_result if @result == []
|
22
|
+
haml :cli
|
23
|
+
rescue ArgumentError
|
24
|
+
wrong_number_of_arguments_for @cmd
|
25
|
+
rescue RuntimeError
|
26
|
+
unknown @cmd
|
27
|
+
rescue Errno::ECONNREFUSED
|
28
|
+
connection_refused
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
post '/config' do
|
33
|
+
param = params[:param].intern
|
34
|
+
value = params[:value]
|
35
|
+
redis.config(:set, param, value) and value
|
36
|
+
end
|
37
|
+
|
38
|
+
get '/stats' do
|
39
|
+
content_type :json
|
40
|
+
redis.zrange(stats_key, count, -1).to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Redmon
|
2
|
+
module Helpers
|
3
|
+
include Redmon::Redis
|
4
|
+
|
5
|
+
def prompt
|
6
|
+
"#{redis_url.gsub('://', ' ')}>"
|
7
|
+
end
|
8
|
+
|
9
|
+
def poll_interval
|
10
|
+
Redmon[:poll_interval] * 1000
|
11
|
+
end
|
12
|
+
|
13
|
+
def count
|
14
|
+
-(params[:count] ? params[:count].to_i : 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
/* Override some defaults */
|
2
|
+
html, body {
|
3
|
+
background-color: #eee;
|
4
|
+
}
|
5
|
+
body {
|
6
|
+
padding-top: 40px; /* 40px to make the container go all the way to the bottom of the topbar */
|
7
|
+
}
|
8
|
+
.container > footer p {
|
9
|
+
text-align: center; /* center align it with the container */
|
10
|
+
}
|
11
|
+
.container {
|
12
|
+
width: 1250px;
|
13
|
+
}
|
14
|
+
.topbar .btns {
|
15
|
+
float: left;
|
16
|
+
margin: 5px 0 0 0;
|
17
|
+
position: relative;
|
18
|
+
filter: alpha(opacity=100);
|
19
|
+
-khtml-opacity: 1;
|
20
|
+
-moz-opacity: 1;
|
21
|
+
opacity: 1;
|
22
|
+
}
|
23
|
+
|
24
|
+
/* The white background content wrapper */
|
25
|
+
.content {
|
26
|
+
background-color: #fff;
|
27
|
+
padding: 20px;
|
28
|
+
margin: 0 -20px; /* negative indent the amount of the padding to maintain the grid system */
|
29
|
+
-webkit-border-radius: 0 0 6px 6px;
|
30
|
+
-moz-border-radius: 0 0 6px 6px;
|
31
|
+
border-radius: 0 0 6px 6px;
|
32
|
+
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
33
|
+
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
34
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Page header tweaks */
|
38
|
+
.page-header {
|
39
|
+
background-color: #f5f5f5;
|
40
|
+
padding: 20px 20px 10px;
|
41
|
+
margin: -20px -20px 20px;
|
42
|
+
}
|
43
|
+
|
44
|
+
.headbar {
|
45
|
+
height:36px;
|
46
|
+
background-color: #f5f5f5;
|
47
|
+
background-image: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#e9e9e9));
|
48
|
+
background-image: -webkit-linear-gradient(top, #f4f4f4, #e9e9e9);
|
49
|
+
background-image: -moz-linear-gradient(top, #f4f4f4, #e9e9e9);
|
50
|
+
background-image: -ms-linear-gradient(top, #f4f4f4, #e9e9e9);
|
51
|
+
background-image: -o-linear-gradient(top, #f4f4f4, #e9e9e9);
|
52
|
+
background-image: linear-gradient(top, #f4f4f4, #e9e9e9);
|
53
|
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#f4f4f4', EndColorStr='#e9e9e9');
|
54
|
+
padding: 0 15px;
|
55
|
+
border-bottom: 1px solid #C9C9C9;
|
56
|
+
border-top: 1px solid #d0d0d0;
|
57
|
+
font-size:13px;
|
58
|
+
line-height:29px;
|
59
|
+
text-shadow: 1px 0px 2px rgba(255, 255, 255, 1);
|
60
|
+
-moz-text-shadow: 1px 0px 2px rgba(255,255,255,1);
|
61
|
+
-webkit-text-shadow: 1px 0px 2px rgba(255,255,255,1);
|
62
|
+
overflow:hidden;
|
63
|
+
}
|
64
|
+
|
65
|
+
.headbar.small{ height:29px; }
|
66
|
+
.headbar h2{ line-height:37px; margin:0; float:left; font-size:14px; }
|
67
|
+
|
68
|
+
.show{ display: block; }
|
69
|
+
.hidden{ display: none; }
|
70
|
+
|
71
|
+
#terminal {
|
72
|
+
margin: 1em 0 0;
|
73
|
+
padding: .25em 0;
|
74
|
+
height: 600px;
|
75
|
+
background: #000;
|
76
|
+
overflow: auto;
|
77
|
+
font-family: Monaco, monospace;
|
78
|
+
}
|
79
|
+
|
80
|
+
#terminal div.line {
|
81
|
+
padding: 0;
|
82
|
+
}
|
83
|
+
|
84
|
+
#terminal input {
|
85
|
+
width: 880px;
|
86
|
+
border: none;
|
87
|
+
display: inline;
|
88
|
+
background: #000;
|
89
|
+
color: #55d839;
|
90
|
+
font-family: Monaco, monospace;
|
91
|
+
}
|
92
|
+
|
93
|
+
#terminal p, #terminal pre {
|
94
|
+
color: #55d839;
|
95
|
+
color: #55d839 !important;
|
96
|
+
margin-bottom: 0px;
|
97
|
+
margin-left: 12px;
|
98
|
+
}
|
99
|
+
|
100
|
+
#terminal a {
|
101
|
+
color: #55d839;
|
102
|
+
}
|
103
|
+
|
104
|
+
#terminal span.prompt {
|
105
|
+
color: #55d839;
|
106
|
+
margin-left: 12px;
|
107
|
+
}
|
108
|
+
|
109
|
+
#terminal input:focus{ box-shadow:none; }
|
110
|
+
|
111
|
+
.chart {
|
112
|
+
width:880px;
|
113
|
+
height:225px;
|
114
|
+
}
|
@@ -0,0 +1,556 @@
|
|
1
|
+
var Redmon = (function() {
|
2
|
+
var config
|
3
|
+
, events = $({});
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Loads the last 100 events and starts the periodic polling for new events.
|
7
|
+
*/
|
8
|
+
function init(opts) {
|
9
|
+
config = opts;
|
10
|
+
toolbar.init();
|
11
|
+
cli.init();
|
12
|
+
requestData(100, function(data) {
|
13
|
+
renderDashboard(data);
|
14
|
+
poll();
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Render the dashboard.
|
20
|
+
*/
|
21
|
+
function renderDashboard(data) {
|
22
|
+
memoryWidget.render(data);
|
23
|
+
keyspaceWidget.render(data);
|
24
|
+
infoWidget.render(data);
|
25
|
+
configWidget.render();
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Request the last {count} events.
|
30
|
+
*/
|
31
|
+
function requestData(count, callback) {
|
32
|
+
$.ajax({
|
33
|
+
url: 'stats?count='+count,
|
34
|
+
success: function(data) {
|
35
|
+
callback(
|
36
|
+
data.map(function(info) {
|
37
|
+
return $.parseJSON(info);
|
38
|
+
})
|
39
|
+
);
|
40
|
+
}
|
41
|
+
});
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Request data from the server, add it to the graph and set a timeout to request again
|
46
|
+
*/
|
47
|
+
function poll() {
|
48
|
+
requestData(1, function(data) {
|
49
|
+
events.trigger('data', data[0]);
|
50
|
+
setTimeout(poll, config.pollInterval);
|
51
|
+
});
|
52
|
+
}
|
53
|
+
|
54
|
+
function formatDate(date) {
|
55
|
+
var d = new Date(parseInt(parseInt(date)));
|
56
|
+
return d.getMonth()+1+'/'+d.getDate()+' '+d.getHours()+':'+d.getMinutes()+':'+d.getSeconds();
|
57
|
+
}
|
58
|
+
|
59
|
+
function formatNumber(num) {
|
60
|
+
return (num + "").replace(/(\d)(?=(\d{3})+(\.\d+|)\b)/g, "$1,");
|
61
|
+
}
|
62
|
+
|
63
|
+
function formatTime(time) {
|
64
|
+
var d = new Date(parseInt(parseInt(time)));
|
65
|
+
return d.getHours()+':'+d.getMinutes();
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* base 1024 formatting for the memory chart y-axis
|
70
|
+
*/
|
71
|
+
function base1024(arg) {
|
72
|
+
var y = arg;
|
73
|
+
if (y >= 1073741824) { return (y / 1073741824).toFixed(2) + "GB" }
|
74
|
+
else if (y >= 1048576) { return (y / 1048576).toFixed(1) + "MB" }
|
75
|
+
else if (y >= 1024) { return (y / 1024).toFixed(0) + "KB" }
|
76
|
+
else if (y < 1 && y > 0) { return y.toFixed(0) }
|
77
|
+
else { return y }
|
78
|
+
}
|
79
|
+
|
80
|
+
//////////////////////////////////////////////////////////////////////
|
81
|
+
// toolbar: nav + event listeners
|
82
|
+
var toolbar = (function() {
|
83
|
+
var mapping = {}
|
84
|
+
, current = {};
|
85
|
+
|
86
|
+
function init() {
|
87
|
+
['dashboard', 'keys', 'cli', 'config'].forEach(function(el) {
|
88
|
+
mapping[el] = $('#'+el)
|
89
|
+
mapping[el].click(onNavClick);
|
90
|
+
});
|
91
|
+
current.tab = mapping.dashboard;
|
92
|
+
current.panel = $('.viewport .dashboard');
|
93
|
+
|
94
|
+
$('#flush-btn').click(function() {
|
95
|
+
$('#flush-confirm').modal({
|
96
|
+
backdrop: true,
|
97
|
+
keyboard: true,
|
98
|
+
show: true
|
99
|
+
});
|
100
|
+
});
|
101
|
+
|
102
|
+
$('#flush-cancel-btn').click(closeModal);
|
103
|
+
|
104
|
+
$('#flush-confirm-btn').click(function() {
|
105
|
+
onBtnClick('flushdb');
|
106
|
+
closeModal();
|
107
|
+
});
|
108
|
+
|
109
|
+
$('#reset-btn').click(function() {
|
110
|
+
onBtnClick('config resetstat');
|
111
|
+
$('#info-tbl').effect("highlight", {}, 2000);
|
112
|
+
});
|
113
|
+
}
|
114
|
+
|
115
|
+
function closeModal() {
|
116
|
+
$('#flush-confirm').modal('hide');
|
117
|
+
}
|
118
|
+
|
119
|
+
function onNavClick(ev) {
|
120
|
+
var tab = $(ev.currentTarget);
|
121
|
+
if (!tab.hasClass('active')) {
|
122
|
+
tab.addClass('active');
|
123
|
+
current.tab.removeClass('active');
|
124
|
+
|
125
|
+
var panel = $('.viewport .'+tab.attr('id'));
|
126
|
+
current.panel.addClass('hidden');
|
127
|
+
panel.removeClass('hidden').addClass('show');
|
128
|
+
|
129
|
+
if (tab.dom === mapping.cli.dom) {
|
130
|
+
cli.focus();
|
131
|
+
}
|
132
|
+
|
133
|
+
current = {tab: tab, panel: panel};
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
function onBtnClick(cmd) {
|
138
|
+
$.ajax({url: 'cli?command='+cmd});
|
139
|
+
}
|
140
|
+
|
141
|
+
return {
|
142
|
+
init: init
|
143
|
+
}
|
144
|
+
})();
|
145
|
+
|
146
|
+
//////////////////////////////////////////////////////////////////////
|
147
|
+
// encapsulate the keyspace chart
|
148
|
+
var memoryWidget = (function() {
|
149
|
+
var plot
|
150
|
+
, dataset;
|
151
|
+
|
152
|
+
function render(data) {
|
153
|
+
dataset = points(data);
|
154
|
+
plot = $.plot('#memory-container', [dataset], {
|
155
|
+
lines: {
|
156
|
+
show: true,
|
157
|
+
fill: true,
|
158
|
+
color: 'rgb(255,50,50)'
|
159
|
+
},
|
160
|
+
points: { show: false },
|
161
|
+
series: { shadowSize: 3 },
|
162
|
+
yaxis: { tickFormatter: base1024},
|
163
|
+
xaxis: { tickFormatter: formatTime},
|
164
|
+
grid: {
|
165
|
+
hoverable: true,
|
166
|
+
clickable: true,
|
167
|
+
backgroundColor: {
|
168
|
+
colors: ['#ddd', '#fff']
|
169
|
+
}
|
170
|
+
}
|
171
|
+
});
|
172
|
+
}
|
173
|
+
|
174
|
+
function points(data) {
|
175
|
+
return data.map(point);
|
176
|
+
}
|
177
|
+
|
178
|
+
function point(info) {
|
179
|
+
return [
|
180
|
+
parseInt(info.time),
|
181
|
+
parseInt(info.used_memory)
|
182
|
+
]
|
183
|
+
}
|
184
|
+
|
185
|
+
function onData(ev, data) {
|
186
|
+
if (data) {
|
187
|
+
if (dataset.length >= 100) {
|
188
|
+
dataset.shift()
|
189
|
+
}
|
190
|
+
|
191
|
+
dataset.push(point(data));
|
192
|
+
plot.setData([dataset]);
|
193
|
+
plot.setupGrid();
|
194
|
+
plot.draw();
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
// observe data events
|
199
|
+
events.bind('data', onData);
|
200
|
+
|
201
|
+
return {
|
202
|
+
render: render
|
203
|
+
}
|
204
|
+
})();
|
205
|
+
|
206
|
+
//////////////////////////////////////////////////////////////////////
|
207
|
+
// encapsulate the keyspace chart
|
208
|
+
var keyspaceWidget = (function() {
|
209
|
+
var plot
|
210
|
+
, dataset = {
|
211
|
+
hits: {label: 'Hits', data: []},
|
212
|
+
misses: {label: 'Misses', data: []},
|
213
|
+
load: function(data) {
|
214
|
+
var self = this;
|
215
|
+
points(data).forEach(function(point) {self.push(point)});
|
216
|
+
return self.series();
|
217
|
+
},
|
218
|
+
append: function(data) {
|
219
|
+
if (this.hits.length >= 100) this.shift();
|
220
|
+
this.push(point(data));
|
221
|
+
return this;
|
222
|
+
},
|
223
|
+
push: function(point) {
|
224
|
+
this.hits.data.push(point[0]);
|
225
|
+
this.misses.data.push(point[1]);
|
226
|
+
},
|
227
|
+
shift: function() {
|
228
|
+
this.hits.data.shift();
|
229
|
+
this.misses.data.shift();
|
230
|
+
},
|
231
|
+
series: function() {
|
232
|
+
return [this.hits, this.misses]
|
233
|
+
}
|
234
|
+
};
|
235
|
+
|
236
|
+
function render(data) {
|
237
|
+
plot = $.plot('#keyspace-container', dataset.load(data), {
|
238
|
+
series: { shadowSize: 3 },
|
239
|
+
legend: { show: false },
|
240
|
+
yaxis: { tickFormatter: formatNumber },
|
241
|
+
xaxis: { tickFormatter: formatTime },
|
242
|
+
grid: {
|
243
|
+
hoverable: true,
|
244
|
+
clickable: true,
|
245
|
+
backgroundColor: {
|
246
|
+
colors: ['#ddd', '#fff']
|
247
|
+
}
|
248
|
+
}
|
249
|
+
});
|
250
|
+
}
|
251
|
+
|
252
|
+
function points(data) {
|
253
|
+
return data.map(point);
|
254
|
+
}
|
255
|
+
|
256
|
+
function point(info) {
|
257
|
+
var time = parseInt(info.time);
|
258
|
+
return [
|
259
|
+
[time, parseInt(info.keyspace_hits)],
|
260
|
+
[time, parseInt(info.keyspace_misses)]
|
261
|
+
];
|
262
|
+
}
|
263
|
+
|
264
|
+
function onData(ev, data) {
|
265
|
+
if (data) {
|
266
|
+
plot.setData(dataset.append(data).series());
|
267
|
+
plot.setupGrid();
|
268
|
+
plot.draw();
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
// observe data events
|
273
|
+
events.bind('data', onData);
|
274
|
+
|
275
|
+
return {
|
276
|
+
render: render
|
277
|
+
}
|
278
|
+
})();
|
279
|
+
|
280
|
+
//////////////////////////////////////////////////////////////////////
|
281
|
+
// encapsulate the info widget
|
282
|
+
var infoWidget = (function() {
|
283
|
+
function render(data) {
|
284
|
+
updateTable(data[data.length-1]);
|
285
|
+
}
|
286
|
+
|
287
|
+
function onData(ev, data) {
|
288
|
+
if (data)
|
289
|
+
updateTable(data);
|
290
|
+
}
|
291
|
+
|
292
|
+
function updateTable(data) {
|
293
|
+
$('#info-tbl td[id]').each(function() {
|
294
|
+
var el = $(this)
|
295
|
+
, field = el.attr('id');
|
296
|
+
|
297
|
+
if (data[field]) {
|
298
|
+
var type = el.attr('type')
|
299
|
+
if (type && type == 'date')
|
300
|
+
el.text(formatDate(data[field]));
|
301
|
+
else if (type && type == 'number')
|
302
|
+
el.text(formatNumber(data[field]))
|
303
|
+
else
|
304
|
+
el.text(data[field]);
|
305
|
+
}
|
306
|
+
});
|
307
|
+
}
|
308
|
+
|
309
|
+
events.bind('data', onData);
|
310
|
+
|
311
|
+
return {
|
312
|
+
render: render
|
313
|
+
}
|
314
|
+
})();
|
315
|
+
|
316
|
+
//////////////////////////////////////////////////////////////////////
|
317
|
+
// encapsulate the slow log widget
|
318
|
+
var slowlogWidget = (function() {
|
319
|
+
|
320
|
+
function render(data) {
|
321
|
+
updateTable(data[data.length-1]);
|
322
|
+
}
|
323
|
+
|
324
|
+
function onData(ev, data) {
|
325
|
+
if (data)
|
326
|
+
updateTable(data);
|
327
|
+
}
|
328
|
+
|
329
|
+
function updateTable(data) {
|
330
|
+
$('#slow-tbl tr').remove();
|
331
|
+
data.slowlog.forEach(function(entry) {
|
332
|
+
$('#slow-tbl').append(
|
333
|
+
$('<tr></tr>')
|
334
|
+
.append(
|
335
|
+
$('<td style="width: 65%; font-weight:bold;"></td>').html(entry.command)
|
336
|
+
).append(
|
337
|
+
$('<td></td>').html((entry.process_time / 1000) + ' ms')
|
338
|
+
).append(
|
339
|
+
$('<td></td>').html(formatDate(entry.timestamp))
|
340
|
+
)
|
341
|
+
);
|
342
|
+
});
|
343
|
+
}
|
344
|
+
|
345
|
+
function formatNumber(num) {
|
346
|
+
return (num + "").replace(/(\d)(?=(\d{3})+(\.\d+|)\b)/g, "$1,");
|
347
|
+
}
|
348
|
+
|
349
|
+
events.bind('data', onData);
|
350
|
+
})();
|
351
|
+
|
352
|
+
//////////////////////////////////////////////////////////////////////
|
353
|
+
// encapsulate the config widget
|
354
|
+
var configWidget = (function() {
|
355
|
+
var selects = {
|
356
|
+
'appendonly' : 'yes,no',
|
357
|
+
'no-appendfsync-on-rewrite' : 'yes,no',
|
358
|
+
'slave-serve-stale-data' : 'yes,no',
|
359
|
+
'loglevel' : 'debug,verbose,notice,warning',
|
360
|
+
'maxmemory-policy' : 'volatile-lru,allkeys-lru,volatile-random,allkeys-random,volatile-ttl,noeviction',
|
361
|
+
'appendfsync' : 'always,everysec,no'
|
362
|
+
};
|
363
|
+
|
364
|
+
function render(data) {
|
365
|
+
$('#config-table .editable').each(function() {
|
366
|
+
var editable = $(this)
|
367
|
+
, id = editable.attr('id');
|
368
|
+
|
369
|
+
var config = {
|
370
|
+
url : '/config',
|
371
|
+
element_id : 'param',
|
372
|
+
update_value : 'value',
|
373
|
+
show_buttons : true,
|
374
|
+
save_button : '<button style="margin-left:5px;"class="btn primary">Save</button>',
|
375
|
+
cancel_button : '<button class="btn">Cancel</button>',
|
376
|
+
default_text : ' '
|
377
|
+
};
|
378
|
+
|
379
|
+
if (selects[id]) {
|
380
|
+
config.field_type = 'select';
|
381
|
+
config.select_options = selects[id];
|
382
|
+
}
|
383
|
+
|
384
|
+
editable.editInPlace(config);
|
385
|
+
});
|
386
|
+
}
|
387
|
+
|
388
|
+
return {
|
389
|
+
render: render
|
390
|
+
}
|
391
|
+
})();
|
392
|
+
|
393
|
+
//////////////////////////////////////////////////////////////////////
|
394
|
+
// terminal emulator
|
395
|
+
var cli = (function() {
|
396
|
+
var terminal;
|
397
|
+
|
398
|
+
function init() {
|
399
|
+
var prompt = [
|
400
|
+
"<div class='line'>" +
|
401
|
+
"<span class='prompt'>"+config.cliPrompt+"</span>" +
|
402
|
+
"<input type='text' class='readLine active' />" +
|
403
|
+
"</div>"
|
404
|
+
].join('');
|
405
|
+
|
406
|
+
terminal = new ReadLine({
|
407
|
+
htmlForInput : function() {return prompt},
|
408
|
+
handler : process
|
409
|
+
});
|
410
|
+
}
|
411
|
+
|
412
|
+
function process(command, callback) {
|
413
|
+
var cmd = command.split(' ')[0];
|
414
|
+
if (!cmds[cmd] === true) {
|
415
|
+
callback("(error) ERR unknown command '"+cmd+"'");
|
416
|
+
return;
|
417
|
+
}
|
418
|
+
|
419
|
+
$.ajax({
|
420
|
+
url : 'cli?command='+command,
|
421
|
+
success : callback
|
422
|
+
});
|
423
|
+
}
|
424
|
+
|
425
|
+
function focus() {
|
426
|
+
terminal.focus();
|
427
|
+
}
|
428
|
+
|
429
|
+
var cmds = {
|
430
|
+
'append' : true,
|
431
|
+
'auth' : true,
|
432
|
+
'bgrewriteaof' : true,
|
433
|
+
'bgsave' : true,
|
434
|
+
'blpop' : true,
|
435
|
+
'brpop' : true,
|
436
|
+
'brpoplpush' : true,
|
437
|
+
'config' : true,
|
438
|
+
'dbsize' : true,
|
439
|
+
'debug' : true,
|
440
|
+
'decr' : true,
|
441
|
+
'decrby' : true,
|
442
|
+
'del' : true,
|
443
|
+
'discard' : true,
|
444
|
+
'echo' : true,
|
445
|
+
'exec' : true,
|
446
|
+
'exists' : true,
|
447
|
+
'expire' : true,
|
448
|
+
'expireat' : true,
|
449
|
+
'flushall' : true,
|
450
|
+
'flushdb' : true,
|
451
|
+
'get' : true,
|
452
|
+
'getbit' : true,
|
453
|
+
'getrange' : true,
|
454
|
+
'getset' : true,
|
455
|
+
'hdel' : true,
|
456
|
+
'hexists' : true,
|
457
|
+
'hget' : true,
|
458
|
+
'hgetall' : true,
|
459
|
+
'hincrby' : true,
|
460
|
+
'hkeys' : true,
|
461
|
+
'hlen' : true,
|
462
|
+
'hmget' : true,
|
463
|
+
'hmset' : true,
|
464
|
+
'hset' : true,
|
465
|
+
'hsetnx' : true,
|
466
|
+
'hvals' : true,
|
467
|
+
'incr' : true,
|
468
|
+
'incrby' : true,
|
469
|
+
'info' : true,
|
470
|
+
'keys' : true,
|
471
|
+
'lastsave' : true,
|
472
|
+
'lindex' : true,
|
473
|
+
'linsert' : true,
|
474
|
+
'llen' : true,
|
475
|
+
'lpop' : true,
|
476
|
+
'lpush' : true,
|
477
|
+
'lpushx' : true,
|
478
|
+
'lrange' : true,
|
479
|
+
'lrem' : true,
|
480
|
+
'lset' : true,
|
481
|
+
'ltrim' : true,
|
482
|
+
'mget' : true,
|
483
|
+
'monitor' : true,
|
484
|
+
'move' : true,
|
485
|
+
'mset' : true,
|
486
|
+
'msetnx' : true,
|
487
|
+
'multi' : true,
|
488
|
+
'object' : true,
|
489
|
+
'persist' : true,
|
490
|
+
'publish' : true,
|
491
|
+
'ping' : true,
|
492
|
+
'quit' : true,
|
493
|
+
'randomkey' : true,
|
494
|
+
'rename' : true,
|
495
|
+
'renamenx' : true,
|
496
|
+
'rpop' : true,
|
497
|
+
'rpoplpush' : true,
|
498
|
+
'rpush' : true,
|
499
|
+
'rpushx' : true,
|
500
|
+
'sadd' : true,
|
501
|
+
'save' : true,
|
502
|
+
'scard' : true,
|
503
|
+
'sdiff' : true,
|
504
|
+
'sdiffstore' : true,
|
505
|
+
'select' : true,
|
506
|
+
'set' : true,
|
507
|
+
'setbit' : true,
|
508
|
+
'setex' : true,
|
509
|
+
'setnx' : true,
|
510
|
+
'setrange' : true,
|
511
|
+
'shutdown' : true,
|
512
|
+
'sinter' : true,
|
513
|
+
'sinterstore' : true,
|
514
|
+
'sismember' : true,
|
515
|
+
'slaveof' : true,
|
516
|
+
'smembers' : true,
|
517
|
+
'smove' : true,
|
518
|
+
'sort' : true,
|
519
|
+
'spop' : true,
|
520
|
+
'srandmember' : true,
|
521
|
+
'srem' : true,
|
522
|
+
'strlen' : true,
|
523
|
+
'sunion' : true,
|
524
|
+
'sunionstore' : true,
|
525
|
+
'sync' : true,
|
526
|
+
'ttl' : true,
|
527
|
+
'type' : true,
|
528
|
+
'watch' : true,
|
529
|
+
'zadd' : true,
|
530
|
+
'zcard' : true,
|
531
|
+
'zcount' : true,
|
532
|
+
'zincrby' : true,
|
533
|
+
'zinterstore' : true,
|
534
|
+
'zrange' : true,
|
535
|
+
'zrangebyscore' : true,
|
536
|
+
'zrank' : true,
|
537
|
+
'zrem' : true,
|
538
|
+
'zremrangebyrank' : true,
|
539
|
+
'zremrangebyscore' : true,
|
540
|
+
'zrevrange' : true,
|
541
|
+
'zrevrangebyscore' : true,
|
542
|
+
'zrevrank' : true,
|
543
|
+
'zscore' : true,
|
544
|
+
'zunionstore' : true
|
545
|
+
}
|
546
|
+
|
547
|
+
return {
|
548
|
+
focus : focus,
|
549
|
+
init : init
|
550
|
+
}
|
551
|
+
})();
|
552
|
+
|
553
|
+
return {
|
554
|
+
init: init
|
555
|
+
}
|
556
|
+
})();
|