redis-stat 0.3.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +85 -0
  5. data/Rakefile +8 -0
  6. data/bin/redis-stat +16 -0
  7. data/lib/redis-stat/constants.rb +118 -0
  8. data/lib/redis-stat/option.rb +134 -0
  9. data/lib/redis-stat/server/public/bootstrap/css/bootstrap-responsive.min.css +9 -0
  10. data/lib/redis-stat/server/public/bootstrap/css/bootstrap.min.css +9 -0
  11. data/lib/redis-stat/server/public/bootstrap/img/glyphicons-halflings-white.png +0 -0
  12. data/lib/redis-stat/server/public/bootstrap/img/glyphicons-halflings.png +0 -0
  13. data/lib/redis-stat/server/public/bootstrap/js/bootstrap.min.js +6 -0
  14. data/lib/redis-stat/server/public/css/site.css +67 -0
  15. data/lib/redis-stat/server/public/favicon.ico +0 -0
  16. data/lib/redis-stat/server/public/favicon.png +0 -0
  17. data/lib/redis-stat/server/public/jqplot/jqplot.canvasAxisLabelRenderer.min.js +57 -0
  18. data/lib/redis-stat/server/public/jqplot/jqplot.canvasAxisTickRenderer.min.js +57 -0
  19. data/lib/redis-stat/server/public/jqplot/jqplot.canvasTextRenderer.min.js +57 -0
  20. data/lib/redis-stat/server/public/jqplot/jquery.jqplot.min.css +1 -0
  21. data/lib/redis-stat/server/public/jqplot/jquery.jqplot.min.js +57 -0
  22. data/lib/redis-stat/server/public/jquery-1.8.2.min.js +2 -0
  23. data/lib/redis-stat/server/public/js/site.js +192 -0
  24. data/lib/redis-stat/server/views/index.erb +145 -0
  25. data/lib/redis-stat/server.rb +57 -0
  26. data/lib/redis-stat/version.rb +3 -0
  27. data/lib/redis-stat.rb +314 -0
  28. data/redis-stat.gemspec +33 -0
  29. data/test/bin_helper.rb +9 -0
  30. data/test/test_redis-stat.rb +193 -0
  31. metadata +194 -0
data/lib/redis-stat.rb ADDED
@@ -0,0 +1,314 @@
1
+ # encoding: utf-8
2
+
3
+ require 'redis-stat/version'
4
+ require 'redis-stat/constants'
5
+ require 'redis-stat/option'
6
+ require 'redis-stat/server' unless RUBY_PLATFORM == 'java'
7
+ require 'insensitive_hash'
8
+ require 'redis'
9
+ require 'tabularize'
10
+ require 'ansi'
11
+ require 'csv'
12
+ require 'parallelize'
13
+ require 'si'
14
+
15
+ class RedisStat
16
+ attr_reader :hosts, :measures
17
+
18
+ def initialize options = {}
19
+ options = RedisStat::Option::DEFAULT.merge options
20
+ @hosts = options[:hosts]
21
+ @redises = @hosts.map { |e|
22
+ host, port = e.split(':')
23
+ Redis.new(Hash[ {:host => host, :port => port, :timeout => DEFAULT_REDIS_TIMEOUT}.select { |k, v| v } ])
24
+ }
25
+ @interval = options[:interval]
26
+ @max_count = options[:count]
27
+ @mono = options[:mono]
28
+ @colors = options[:colors] || COLORS
29
+ @csv = options[:csv]
30
+ @auth = options[:auth]
31
+ @measures = MEASURES[ options[:verbose] ? :verbose : :default ]
32
+ @all_measures= MEASURES.values.inject(:+).uniq - [:at]
33
+ @count = 0
34
+ @style = options[:style]
35
+ @first_batch = true
36
+ @server_port = options[:server_port]
37
+ @daemonized = options[:daemon]
38
+ end
39
+
40
+ def info
41
+ collect
42
+ end
43
+
44
+ def start output_stream
45
+ @os = output_stream
46
+ trap('INT') { Thread.main.raise Interrupt }
47
+
48
+ begin
49
+ csv = File.open(@csv, 'w') if @csv
50
+ update_term_size!
51
+
52
+ # Warm-up / authenticate only when needed
53
+ @redises.each do |r|
54
+ begin
55
+ r.info
56
+ rescue Redis::CommandError
57
+ r.auth @auth if @auth
58
+ end
59
+ end
60
+
61
+
62
+ @started_at = Time.now
63
+ prev_info = nil
64
+ server = start_server if @server_port
65
+
66
+ loop do
67
+ info = collect
68
+ info_output = process info, prev_info
69
+ output info, info_output, csv unless @daemonized
70
+ server.push info, Hash[info_output] if server
71
+ prev_info = info
72
+
73
+ @count += 1
74
+ break if @max_count && @count >= @max_count
75
+ sleep @interval
76
+ end
77
+ @os.puts
78
+ rescue Interrupt
79
+ @os.puts
80
+ @os.puts ansi(:yellow, :bold) { "Interrupted." }
81
+ rescue Exception => e
82
+ @os.puts ansi(:red, :bold) { e.to_s }
83
+ raise
84
+ ensure
85
+ csv.close if csv
86
+ end
87
+ @os.puts ansi(:blue, :bold) {
88
+ "Elapsed: #{"%.2f" % (Time.now - @started_at)} sec."
89
+ }
90
+ end
91
+
92
+ private
93
+ def start_server
94
+ RedisStat::Server.set :port, @server_port
95
+ RedisStat::Server.set :redis_stat, self
96
+ Thread.new { RedisStat::Server.run! }
97
+ RedisStat::Server.wait_until_running
98
+ trap('INT') { Thread.main.raise Interrupt }
99
+ RedisStat::Server
100
+ end
101
+
102
+ def collect
103
+ {}.insensitive.tap do |info|
104
+ class << info
105
+ def sumf label
106
+ (self[label] || []).map(&:to_f).inject(:+)
107
+ end
108
+ end
109
+
110
+ info[:at] = Time.now.to_f
111
+ @redises.pmap(@redises.length) { |redis|
112
+ redis.info.insensitive
113
+ }.each do |rinfo|
114
+ (@all_measures + rinfo.keys.select { |k| k =~ /^db[0-9]+$/ }).each do |k|
115
+ info[k] ||= []
116
+ info[k] << rinfo[k]
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def update_term_size!
123
+ if RUBY_PLATFORM == 'java'
124
+ require 'java'
125
+ begin
126
+ case JRUBY_VERSION
127
+ when /^1\.7/
128
+ @term ||= Java::jline.console.ConsoleReader.new.getTerminal
129
+ @term_width = (@term.width rescue DEFAULT_TERM_WIDTH)
130
+ @term_height = (@term.height rescue DEFAULT_TERM_HEIGHT) - 4
131
+ return
132
+ when /^1\.6/
133
+ @term ||= Java::jline.ConsoleReader.new.getTerminal
134
+ @term_width = (@term.getTerminalWidth rescue DEFAULT_TERM_WIDTH)
135
+ @term_height = (@term.getTerminalHeight rescue DEFAULT_TERM_HEIGHT) - 4
136
+ return
137
+ end
138
+ rescue Exception
139
+ # Fallback to tput (which yields incorrect values as of now)
140
+ end
141
+ end
142
+
143
+ @term_width = (`tput cols` rescue DEFAULT_TERM_WIDTH).to_i
144
+ @term_height = (`tput lines` rescue DEFAULT_TERM_HEIGHT).to_i - 4
145
+ end
146
+
147
+ def move! lines
148
+ return if lines == 0
149
+
150
+ @os.print(
151
+ if defined?(Win32::Console)
152
+ if lines < 0
153
+ "\e[#{- lines}F"
154
+ else
155
+ "\e[#{lines}E"
156
+ end
157
+ else
158
+ if lines < 0
159
+ "\e[#{- lines}A\e[0G"
160
+ else
161
+ "\e[#{lines}B\e[0G"
162
+ end
163
+ end)
164
+ end
165
+
166
+ def output info, info_output, file
167
+ @table ||= init_table info_output
168
+
169
+ movement = nil
170
+ if @count == 0
171
+ output_static_info info
172
+
173
+ movement = 0
174
+ if file
175
+ file.puts CSV.generate_line(info_output.map { |pair|
176
+ LABELS[pair.first] || pair.first
177
+ })
178
+ end
179
+ elsif @count % @term_height == 0
180
+ @first_batch = false
181
+ movement = -1
182
+ update_term_size!
183
+ @table = init_table info_output
184
+ end
185
+
186
+ # Build output table
187
+ @table << info_output.map { |pair|
188
+ ansi(*@colors[pair.first]) { [*pair.last].first }
189
+ }
190
+ lines = @table.to_s.lines.map(&:chomp)
191
+ lines.delete_at @first_batch ? 1 : 0
192
+ width = lines.first.length
193
+ height = lines.length
194
+
195
+ # Calculate the number of lines to go upward
196
+ if movement.nil?
197
+ if @prev_width && @prev_width == width
198
+ lines = lines[-2..-1]
199
+ movement = -1
200
+ else
201
+ movement = -(height - 1)
202
+ end
203
+ end
204
+ @prev_width = width
205
+
206
+ move! movement
207
+ begin
208
+ @os.print $/ + lines.join($/)
209
+
210
+ if file
211
+ file.puts CSV.generate_line(info_output.map { |pair|
212
+ [*pair.last].last
213
+ })
214
+ end
215
+ rescue Interrupt
216
+ move! -movement
217
+ raise
218
+ end
219
+ end
220
+
221
+ def output_static_info info
222
+ tab = Tabularize.new(
223
+ :unicode => false, :align => :right,
224
+ :border_style => @style
225
+ )
226
+ tab << [nil] + @hosts.map { |h| ansi(:bold, :green) { h } }
227
+ tab.separator!
228
+ MEASURES[:static].each do |key|
229
+ tab << [ansi(:bold) { key }] + info[key] unless info[key].compact.empty?
230
+ end
231
+ @os.puts tab
232
+ end
233
+
234
+ def init_table info_output
235
+ table = Tabularize.new :unicode => false,
236
+ :align => :right,
237
+ :border_style => @style,
238
+ :border_color => @mono ? nil : ANSI::Code.red,
239
+ :vborder => ' ',
240
+ :pad_left => 0,
241
+ :pad_right => 0,
242
+ :screen_width => @term_width
243
+ table.separator!
244
+ table << info_output.map { |pair|
245
+ ansi(*((@colors[pair.first] || []) + [:underline])) {
246
+ LABELS[pair.first] || pair.first
247
+ }
248
+ }
249
+ table.separator!
250
+ table
251
+ end
252
+
253
+ def process info, prev_info
254
+ @measures.map { |key|
255
+ [ key, process_how(info, prev_info, key) ]
256
+ }.select { |pair| pair.last }
257
+ end
258
+
259
+ def process_how info, prev_info, key
260
+ dur = prev_info && (info[:at] - prev_info[:at])
261
+
262
+ get_diff = lambda do |label|
263
+ if dur && dur > 0
264
+ (info.sumf(label) - prev_info.sumf(label)) / dur
265
+ else
266
+ nil
267
+ end
268
+ end
269
+
270
+ case key
271
+ when :at
272
+ val = Time.now.strftime('%H:%M:%S')
273
+ [val, val]
274
+ when :used_cpu_user, :used_cpu_sys
275
+ val = get_diff.call(key)
276
+ val &&= (val * 100).round
277
+ [humanize_number(val), val]
278
+ when :keys
279
+ val = Hash[ info.select { |k, v| k =~ /^db[0-9]+$/ } ].values.inject(0) { |sum, vs|
280
+ sum + vs.map { |v| Hash[ v.split(',').map { |e| e.split '=' } ]['keys'].to_i }.inject(:+)
281
+ }
282
+ [humanize_number(val), val]
283
+ when :evicted_keys_per_second, :expired_keys_per_second, :keyspace_hits_per_second,
284
+ :keyspace_misses_per_second, :total_commands_processed_per_second
285
+ val = get_diff.call(key.to_s.gsub(/_per_second$/, '').to_sym)
286
+ [humanize_number(val), val]
287
+ when :used_memory, :used_memory_rss, :aof_current_size, :aof_base_size
288
+ val = info.sumf(key)
289
+ [humanize_number(val.to_i, true), val]
290
+ else
291
+ val = info.sumf(key)
292
+ [humanize_number(val), val]
293
+ end
294
+ end
295
+
296
+ def humanize_number num, byte = false
297
+ return '-' if num.nil?
298
+
299
+ num = num.to_i if num == num.to_i
300
+ if byte
301
+ num.si_byte
302
+ else
303
+ num.si(:min_exp => 0)
304
+ end
305
+ end
306
+
307
+ def ansi *args, &block
308
+ if @mono || args.empty?
309
+ block.call
310
+ else
311
+ ANSI::Code.ansi *args, &block
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/redis-stat/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Junegunn Choi"]
6
+ gem.email = ["junegunn.c@gmail.com"]
7
+ gem.description = %q{A Redis monitoring tool written in Ruby}
8
+ gem.summary = %q{A Redis monitoring tool written in Ruby}
9
+ gem.homepage = "https://github.com/junegunn/redis-stat"
10
+
11
+ gem.platform = 'java' if RUBY_PLATFORM == 'java'
12
+ gem.files = `git ls-files`.split("\n").reject { |f| f =~ /^screenshots/ }
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = "redis-stat"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = RedisStat::VERSION
18
+
19
+ gem.add_runtime_dependency "ansi", '~> 1.4.3'
20
+ gem.add_runtime_dependency "redis", '~> 3.0.1'
21
+ gem.add_runtime_dependency "tabularize", '~> 0.2.8'
22
+ gem.add_runtime_dependency "insensitive_hash", '~> 0.3.0'
23
+ gem.add_runtime_dependency "parallelize", '~> 0.4.0'
24
+ gem.add_runtime_dependency "si", '~> 0.1.3'
25
+ unless RUBY_PLATFORM == 'java'
26
+ gem.add_runtime_dependency "sinatra", '~> 1.3.3'
27
+ gem.add_runtime_dependency "thin", '~> 1.5.0'
28
+ gem.add_runtime_dependency "json", '~> 1.7.5'
29
+ gem.add_runtime_dependency "daemons", '~> 1.1.9'
30
+ end
31
+
32
+ gem.add_development_dependency 'test-unit'
33
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ # DISCLAIMER:
5
+ # Not a real test!
6
+ # Just a helper script for running scripts with local source
7
+
8
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib')
9
+ load File.join(File.dirname(__FILE__), '../bin/', File.basename(ARGV.shift))
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'rubygems'
5
+ require 'test-unit'
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ require 'redis-stat'
8
+ require 'redis'
9
+ require 'stringio'
10
+
11
+ class TestRedisStat < Test::Unit::TestCase
12
+ def test_humanize_number
13
+ rs = RedisStat.new
14
+ assert_equal '0', rs.send(:humanize_number, 0.00)
15
+ assert_equal '7', rs.send(:humanize_number, 7)
16
+ assert_equal '0.01', rs.send(:humanize_number, 0.00751)
17
+ assert_equal '0.08', rs.send(:humanize_number, 0.0751)
18
+ assert_equal '0.75', rs.send(:humanize_number, 0.751)
19
+ assert_equal '7.51', rs.send(:humanize_number, 7.51)
20
+ assert_equal '75.1', rs.send(:humanize_number, 75.1)
21
+ assert_equal '7.51k', rs.send(:humanize_number, 7510)
22
+ assert_equal '75.1k', rs.send(:humanize_number, 75100)
23
+ assert_equal '751k', rs.send(:humanize_number, 751000)
24
+ assert_equal '7.51M', rs.send(:humanize_number, 7510000)
25
+ assert_equal '75.1M', rs.send(:humanize_number, 75100000)
26
+ assert_equal '751M', rs.send(:humanize_number, 751000000)
27
+ assert_equal '7.51G', rs.send(:humanize_number, 7510000000)
28
+ assert_equal '75.1G', rs.send(:humanize_number, 75100000000)
29
+ assert_equal '751G', rs.send(:humanize_number, 751000000000)
30
+ assert_equal '7.51T', rs.send(:humanize_number, 7510000000000)
31
+ assert_equal '75.1T', rs.send(:humanize_number, 75100000000000)
32
+ assert_equal '751T', rs.send(:humanize_number, 751000000000000)
33
+ assert_equal '7.51P', rs.send(:humanize_number, 7510000000000000)
34
+ assert_equal '75.1P', rs.send(:humanize_number, 75100000000000000)
35
+ assert_equal '751P', rs.send(:humanize_number, 751000000000000000)
36
+ assert_equal '7.51E', rs.send(:humanize_number, 7510000000000000000)
37
+ assert_equal '75.1E', rs.send(:humanize_number, 75100000000000000000)
38
+ assert_equal '751E', rs.send(:humanize_number, 751000000000000000000)
39
+ assert_equal '7.51Z', rs.send(:humanize_number, 7510000000000000000000)
40
+
41
+ assert_equal '7.51PB', rs.send(:humanize_number, 7.51 * (1024 ** 5), true)
42
+ assert_equal '-7.51PB', rs.send(:humanize_number, -7.51 * (1024 ** 5), true)
43
+ end
44
+
45
+ def test_option_parse
46
+ options = RedisStat::Option.parse([])
47
+ assert_equal RedisStat::Option::DEFAULT.sort, options.sort
48
+
49
+ options = RedisStat::Option.parse(%w[localhost:1000 20])
50
+ assert_equal({
51
+ :hosts => ['localhost:1000'],
52
+ :interval => 20,
53
+ :count => nil,
54
+ :csv => nil,
55
+ :style => :unicode
56
+ }.sort, options.sort)
57
+
58
+ options = RedisStat::Option.parse(%w[localhost:1000 20 30])
59
+ assert_equal({
60
+ :hosts => ['localhost:1000'],
61
+ :interval => 20,
62
+ :count => 30,
63
+ :csv => nil,
64
+ :style => :unicode
65
+ }.sort, options.sort)
66
+
67
+ options = RedisStat::Option.parse(%w[20])
68
+ assert_equal({
69
+ :hosts => ['127.0.0.1:6379'],
70
+ :interval => 20,
71
+ :count => nil,
72
+ :csv => nil,
73
+ :style => :unicode
74
+ }.sort, options.sort)
75
+
76
+ options = RedisStat::Option.parse(%w[20 30])
77
+ assert_equal({
78
+ :hosts => ['127.0.0.1:6379'],
79
+ :interval => 20,
80
+ :count => 30,
81
+ :csv => nil,
82
+ :style => :unicode
83
+ }.sort, options.sort)
84
+
85
+ options = RedisStat::Option.parse(%w[localhost:8888 10 --csv=/tmp/a.csv --style=ascii --auth password])
86
+ assert_equal({
87
+ :auth => 'password',
88
+ :hosts => ['localhost:8888'],
89
+ :interval => 10,
90
+ :count => nil,
91
+ :csv => '/tmp/a.csv',
92
+ :style => :ascii
93
+ }.sort, options.sort)
94
+
95
+ options = RedisStat::Option.parse(%w[-h localhost:8888 10 -a password --csv=/tmp/a.csv --style=ascii])
96
+ assert_equal({
97
+ :auth => 'password',
98
+ :hosts => ['localhost:8888'],
99
+ :interval => 10,
100
+ :count => nil,
101
+ :csv => '/tmp/a.csv',
102
+ :style => :ascii
103
+ }.sort, options.sort)
104
+
105
+ # Server
106
+ if RUBY_PLATFORM == 'java'
107
+ assert_raise(SystemExit) {
108
+ RedisStat::Option.parse(%w[-h localhost:8888 10 -a password --csv=/tmp/a.csv --style=ascii --server=5555])
109
+ }
110
+ assert_raise(SystemExit) {
111
+ RedisStat::Option.parse(%w[-h localhost:8888 10 -a password --csv=/tmp/a.csv --style=ascii --server=5555 --daemon])
112
+ }
113
+ else
114
+ options = RedisStat::Option.parse(%w[-h localhost:8888 10 -a password --csv=/tmp/a.csv --style=ascii --server=5555 --daemon])
115
+ assert_equal({
116
+ :auth => 'password',
117
+ :hosts => ['localhost:8888'],
118
+ :interval => 10,
119
+ :count => nil,
120
+ :csv => '/tmp/a.csv',
121
+ :server_port => "5555",
122
+ :style => :ascii,
123
+ :daemon => true
124
+ }.sort, options.sort)
125
+ end
126
+
127
+ options = RedisStat::Option.parse(%w[--no-color])
128
+ assert_equal true, options[:mono]
129
+ end
130
+
131
+ def test_option_parse_invalid
132
+ [
133
+ %w[localhost 0],
134
+ %w[localhost 5 0]
135
+ ].each do |argv|
136
+ assert_raise(SystemExit) {
137
+ options = RedisStat::Option.parse(argv)
138
+ }
139
+ end
140
+
141
+ assert_raise(SystemExit) {
142
+ RedisStat::Option.parse(%w[--style=html])
143
+ }
144
+
145
+ assert_raise(SystemExit) {
146
+ RedisStat::Option.parse(%w[--daemon])
147
+ }
148
+ end
149
+
150
+ def test_start
151
+ csv = '/tmp/redis-stat.csv'
152
+ cnt = 100
153
+ rs = RedisStat.new :hosts => %w[localhost] * 5, :interval => 0.01, :count => cnt,
154
+ :verbose => true, :csv => csv, :auth => 'pw'
155
+ rs.start $stdout
156
+
157
+ assert_equal cnt + 1, File.read(csv).lines.to_a.length
158
+ ensure
159
+ File.unlink csv
160
+ end
161
+
162
+ def test_mono
163
+ [true, false].each do |mono|
164
+ rs = RedisStat.new :hosts => %w[localhost] * 5, :interval => 0.02, :count => 20,
165
+ :verbose => true, :auth => 'pw', :mono => mono
166
+ output = StringIO.new
167
+ rs.start output
168
+ puts output.string
169
+ assert_equal mono, output.string !~ /\e\[\d*(;\d+)*m/
170
+ end
171
+ end
172
+
173
+ def test_static_info_of_mixed_versions
174
+ # prerequisite
175
+ r1 = Redis.new(:host => 'localhost')
176
+ r2 = Redis.new(:host => 'localhost', :port => 6380)
177
+
178
+ if r1.info['redis_version'] =~ /^2\.4/ && r2.info['redis_version'] =~ /^2\.2/
179
+ rs = RedisStat.new :hosts => %w[localhost:6380 localhost], :interval => 1, :count => 1,
180
+ :auth => 'pw', :style => :ascii
181
+ output = StringIO.new
182
+ rs.start output
183
+ vline = output.string.lines.select { |line| line =~ /gcc_version/ }.first
184
+ puts vline.gsub(/ +/, ' ')
185
+ assert vline.gsub(/ +/, ' ').include?('| | 4.2.1 |')
186
+ else
187
+ raise NotImplementedError.new # FIXME
188
+ end
189
+ rescue Redis::CannotConnectError, NotImplementedError
190
+ pend "redises not ready"
191
+ end
192
+ end
193
+