redis-stat 0.3.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+