mmtop 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ require 'rubygems'
5
+ require 'bundler'
6
+ Bundler.setup
7
+
8
+ require 'pp'
9
+ require 'Getopt/Declare'
10
+
11
+ require 'mysql2'
12
+ require 'lib/mmconfig'
13
+ require 'lib/host'
14
+ require 'lib/process'
15
+ require 'lib/term_printer'
16
+ require 'lib/term_input'
17
+ require 'lib/command'
18
+ require 'lib/pid'
19
+ require 'lib/filter'
20
+
21
+
22
+ spec = <<-EOL
23
+ -c, --config <FILE> Where to find mmtop_config
24
+ -p, --password <pass> The password used to connect to all DBs
25
+ -u, --user <user> The user used to connect to all DBs
26
+ -o, --ops David's funky ops mode
27
+ EOL
28
+
29
+ args = Getopt::Declare.new(spec)
30
+ config = MMTop::Config.new(args)
31
+
32
+ printer = MMTop::TermPrinter.new
33
+ input = MMTop::TermInput.new
34
+
35
+ while true
36
+ MMTop::PID.reset
37
+ config.get_info
38
+ printer.print_info(config.info)
39
+ input.control(config)
40
+ end
41
+
42
+
43
+
@@ -0,0 +1,45 @@
1
+ module MMTop
2
+ class Command
3
+ def matches?(cmd)
4
+ @regexp.match(cmd)
5
+ end
6
+
7
+ def run(cmd, config)
8
+ cmd =~ @regexp
9
+ @command.call(cmd, config)
10
+ end
11
+
12
+ def regexp(r = nil)
13
+ @regexp = r if r
14
+ @regexp
15
+ end
16
+
17
+ def usage(u = nil)
18
+ @usage = u if u
19
+ @usage
20
+ end
21
+
22
+ def explain(e = nil)
23
+ @explain = e if e
24
+ @explain
25
+ end
26
+
27
+ def command(&block)
28
+ @command = block.to_proc
29
+ end
30
+
31
+ class <<self
32
+ def register
33
+ @commands ||= []
34
+ c = self.new
35
+ yield c
36
+ @commands << c
37
+ end
38
+
39
+ attr_accessor :commands
40
+ end
41
+ end
42
+ end
43
+
44
+ Dir.glob(File.join(File.dirname(__FILE__), "commands/*")).each { |f| require f }
45
+
@@ -0,0 +1,96 @@
1
+ MMTop::Command.register do |c|
2
+ c.regexp /^h|help|\?/
3
+ c.usage "help"
4
+ c.explain "Display this text"
5
+ c.command do |cmd, config|
6
+ MMTop::Command.commands.sort_by(&:usage).each do |c|
7
+ puts "%-50s %s" % [c.usage, c.explain]
8
+ end
9
+ end
10
+ end
11
+
12
+ MMTop::Command.register do |c|
13
+ c.regexp /^q|quit|\?/
14
+ c.usage "quit"
15
+ c.explain "Exit"
16
+ c.command do |cmd, config|
17
+ exit
18
+ end
19
+ end
20
+
21
+
22
+ MMTop::Command.register do |c|
23
+ c.regexp /^[x|(?:examine)]\s+(\d+)/
24
+ c.usage "x PID"
25
+ c.explain "Show full query"
26
+ c.command do |cmd, config|
27
+ cmd =~ c.regexp
28
+ pid = $1.to_i
29
+ ps = config.find_pid(pid)
30
+ if ps
31
+ puts "%-20s%-6s%-20s%-20s%-20s" % ["status","time","client", "server", "database"]
32
+ puts "%-20s%-6s%-20s%-20s%-20s" % [ps.status, ps.time, ps.client, ps.host.name, ps.db]
33
+ puts ps.sql
34
+ else
35
+ puts "No such pid #{p}"
36
+ end
37
+ end
38
+ end
39
+
40
+ MMTop::Command.register do |c|
41
+ c.regexp /^(?:ex|explain)\s+(\d+)/
42
+ c.usage "explain PID"
43
+ c.explain "Show query plan"
44
+ c.command do |cmd, config|
45
+ cmd =~ c.regexp
46
+ pid = $1.to_i
47
+ ps = config.find_pid(pid)
48
+ if ps
49
+ puts ps.sql
50
+ explain = ps.explain
51
+ pp explain
52
+ else
53
+ puts "No such pid #{p}"
54
+ end
55
+ end
56
+ end
57
+
58
+ MMTop::Command.register do |c|
59
+ c.regexp /^(?:l|list)\s+(\S+)/
60
+ c.usage "list HOST"
61
+ c.explain "List hosts and their connections to this server"
62
+ c.command do |cmd, config|
63
+ cmd =~ c.regexp
64
+ host = $1
65
+
66
+ server = config.find_server(host)
67
+ if server
68
+ cxs = {}
69
+ server.connections.each do |cx|
70
+ client = MMTop::ReverseLookup.lookup(cx.client)
71
+ cxs[client] ||= 0
72
+ cxs[client] += 1
73
+ end
74
+
75
+ cxs.sort_by { |k, v| [-v, k] }.each { |k, v|
76
+ puts "#{k}: #{v}"
77
+ }
78
+ else
79
+ puts "No such host: #{server}"
80
+ end
81
+ end
82
+ end
83
+
84
+ MMTop::Command.register do |c|
85
+ c.regexp /^sleep\s+([\d\.]+)/
86
+ c.usage "sleep TIME"
87
+ c.explain "Set mmtop sleep time"
88
+ c.command do |cmd, config|
89
+ cmd =~ c.regexp
90
+ sleep = $1.to_f
91
+ if sleep == 0.0
92
+ puts "sleep must be over 0."
93
+ end
94
+ config.options['sleep'] = sleep
95
+ end
96
+ end
@@ -0,0 +1,73 @@
1
+ MMTop::Command.register do |c|
2
+ c.regexp /^filter\s+list/
3
+ c.usage "filter list"
4
+ c.explain "Show active filters"
5
+ c.command do |cmd, config|
6
+ config.filters.each_with_index do |f, i|
7
+ puts "#{i}: #{f.name}"
8
+ end
9
+ end
10
+ end
11
+
12
+ MMTop::Command.register do |c|
13
+ c.regexp /^filter\s+available/
14
+ c.usage "filter available"
15
+ c.explain "Show available filters"
16
+ c.command do |cmd, config|
17
+ MMTop::Filter.filters.each do |o|
18
+ puts "#{o.name}"
19
+ end
20
+ end
21
+ end
22
+
23
+ MMTop::Command.register do |c|
24
+ c.regexp /^filter\s+add/
25
+ c.usage "filter add [{ BLOCK } | NAME] [POSITION]"
26
+ c.explain "Add a new filter into the chain. For the { BLOCK } form, the block has access to the \'query\' variable. see docs for more info."
27
+ c.command do |cmd, config|
28
+ cmd =~ /filter\s+add(.*)/
29
+
30
+ filter = nil
31
+ position = nil
32
+ rest = $1
33
+ if rest =~ /\{(.*)\}\s*(\d*)/
34
+ eval_bits = $1.strip
35
+ position = $2
36
+ begin
37
+ filter = MMTop::Filter.from_string(eval_bits)
38
+ #rescue Exception => e
39
+ # puts "error evaluating filter block: #{e}"
40
+ end
41
+ else
42
+ name, position = rest.strip.split(/\s+/)
43
+ filter = MMTop::Filter.filters.detect { |f| f.name == name }
44
+ if filter.nil?
45
+ puts "No such filter: '#{name}'"
46
+ end
47
+ end
48
+
49
+ if filter
50
+ if position && !position.empty?
51
+ config.filters.insert(position.to_i, filter)
52
+ else
53
+ config.filters.push(filter)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ MMTop::Command.register do |c|
60
+ c.regexp /^filter\s+rm\s+(\d+)/
61
+ c.usage "filter rm [POSITION]"
62
+ c.explain "Remove the filter at [POSITION]"
63
+ c.command do |cmd, config|
64
+ cmd =~ c.regexp
65
+ pos = $1.to_i
66
+ if !config.filters[pos]
67
+ puts "No filter at #{pos}"
68
+ else
69
+ config.filters.slice!(pos)
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,75 @@
1
+ module MMTop
2
+ class Command
3
+ def self.kill_prompt(queries)
4
+ if queries.empty?
5
+ puts "No queries matched."
6
+ return
7
+ end
8
+
9
+ puts "killing: "
10
+ queries.each_with_index do |q, i|
11
+ puts "#{i}: #{q.host.name}\t\t#{q.sql[0..80]}"
12
+ end
13
+
14
+ print "Please confirm (y|n)> "
15
+ if $stdin.readline != "y\n"
16
+ puts "no, ok."
17
+ else
18
+ queries.each(&:kill!)
19
+ puts "killed."
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ MMTop::Command.register do |c|
26
+ c.regexp /^k(:?ill)?(\s+\d+)/
27
+ c.usage "kill [PID] [..PID]"
28
+ c.explain "Kill a number of queries by PID"
29
+ c.command do |cmd, config|
30
+ pids = []
31
+ procs = []
32
+ cmd.gsub(/\s+\d+/) { |p| pids << p.to_i }
33
+
34
+ pids.each { |p|
35
+ ps = config.find_pid(p)
36
+ if ps.nil?
37
+ puts "No such pid: #{p}"
38
+ next
39
+ end
40
+ procs << ps
41
+ }
42
+ procs.each { |ps| ps.kill! }
43
+ end
44
+ end
45
+
46
+ MMTop::Command.register do |c|
47
+ c.regexp %r{^k(:?ill)?\s+l(:?ong)?\s+(\d+)}
48
+ c.usage "kill long TIME"
49
+ c.explain "Kill any queries over TIME seconds"
50
+ c.command do |cmd, config|
51
+ cmd =~ c.regexp
52
+ time = $3.to_i
53
+ MMTop::Command.kill_prompt(config.all_processes.select { |p| p.time > time && p.sql =~ /select/i })
54
+ end
55
+ end
56
+
57
+
58
+
59
+
60
+ MMTop::Command.register do |c|
61
+ c.regexp %r{^k(ill)?\s+(/.*/\w*)}
62
+ c.usage "kill /REGEXP/"
63
+ c.explain "Kill a number of queries by REGEXP"
64
+ c.command do |cmd, config|
65
+ cmd =~ c.regexp
66
+ r = eval($2)
67
+ if !r.is_a?(Regexp)
68
+ puts "Invalid regexp \"#{$1}\""
69
+ else
70
+ MMTop::Command.kill_prompt(config.all_processes.select { |p| r.match(p.sql) })
71
+ end
72
+ end
73
+ end
74
+
75
+
@@ -0,0 +1,43 @@
1
+ module MMTop
2
+ class Filter
3
+ class << self
4
+ attr_accessor :filters
5
+
6
+ def add_filter(name, default=true, &block)
7
+ @filters ||= []
8
+ @filters.push self.new(name, default, &block)
9
+ end
10
+
11
+ def default_filters
12
+ @filters.select(&:default)
13
+ end
14
+
15
+ def from_string(string)
16
+ final = string.sub(/^\s*\{/, '').sub(/\}$/, '')
17
+ p = eval("Proc.new { |qlist| qlist.reject! { |query| !(#{final}) } }")
18
+ new(string, false, &p)
19
+ end
20
+
21
+ end
22
+
23
+ def initialize(name, default, &block)
24
+ @name = name
25
+ @default = default
26
+ @block = block.to_proc
27
+ end
28
+
29
+ def run(query_list, host, config)
30
+ case @block.arity
31
+ when 1
32
+ @block.call(query_list)
33
+ when 2
34
+ @block.call(query_list, host)
35
+ when 3
36
+ @block.call(query_list, host, config)
37
+ end
38
+ end
39
+ attr_accessor :name, :default
40
+ end
41
+ end
42
+
43
+ Dir.glob(File.join(File.dirname(__FILE__), "filters/*")).each { |f| require f }
@@ -0,0 +1,31 @@
1
+ module MMTop
2
+ Filter.add_filter('strip_empty') do |queries|
3
+ queries.reject! { |q| q.sql.nil? || q.sql.strip.empty? }
4
+ end
5
+
6
+ Filter.add_filter('trim_whitespace') do |queries|
7
+ queries.each { |q| q.sql.gsub!(/\s+/, ' ') }
8
+ end
9
+
10
+ Filter.add_filter('sort_by_time') do |queries|
11
+ queries.sort! { |a, b| b.time <=> a.time }
12
+ end
13
+
14
+ WEDGE_THRESHOLD=15
15
+ Filter.add_filter('wedge_monitor') do |queries, hostinfo, config|
16
+ if hostinfo.host.wedge_monitor?
17
+ if queries.size > WEDGE_THRESHOLD
18
+ begin
19
+ wedge_filename = config.options['wedge_log'] || 'mmtop_wedge_log'
20
+ File.open(wedge_filename, "a+") { |f|
21
+ f.puts("Wedge detected on #{hostinfo.host.name}. Dumping innodb status")
22
+ q = hostinfo.host.query("show innodb status")
23
+ f.write(q[0][:Status])
24
+ }
25
+ rescue Exception => e
26
+ $stderr.puts("error writing to wedge log: #{e}:#{e.message}")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,115 @@
1
+ module MMTop
2
+ class Topology
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def new_hostlist
8
+ topology = find_master_slave
9
+ topology.each { |name, t| fill_chain_info(t, topology) }
10
+
11
+ new_top = create_sort_array(topology)
12
+
13
+ hosts = @config.hosts.sort_by { |h| new_top.find_index { |t| t[:hostname] == h.name } }
14
+ hosts.each { |h|
15
+ top = new_top.find { |t| t[:hostname] == h.name }
16
+ if top[:levels] > 0
17
+ h.display_name = (" " * top[:levels]) + '\_' + h.name
18
+ end
19
+ }
20
+ hosts
21
+ end
22
+
23
+ def insert_host_into_sort_array(t, host, array)
24
+ array << host
25
+ t.select { |k, v|
26
+ # find hosts who are our slaves
27
+ v[:master] == host[:hostname]
28
+ }.sort_by { |k, v|
29
+ # add those without children of their own first
30
+ v[:is_master].to_i
31
+ }.each { |k, s|
32
+ insert_host_into_sort_array(t, s, array)
33
+ }
34
+ array
35
+ end
36
+
37
+
38
+ def create_sort_array(t)
39
+ array = []
40
+ t.values.select { |v|
41
+ v[:levels] == 0
42
+ }.sort_by { |v|
43
+ v[:hostname]
44
+ }.each { |v|
45
+ insert_host_into_sort_array(t, v, array)
46
+ }
47
+ array
48
+ end
49
+
50
+ def fill_chain_info(host, topology)
51
+ levels = 0
52
+ stack = []
53
+ master = host
54
+ while master = topology[master[:master]]
55
+ # loop detection
56
+ break if stack.include?(master)
57
+
58
+ last_master = master
59
+ levels += 1
60
+ stack.push(master)
61
+ end
62
+
63
+ if last_master
64
+ host[:final_master] = last_master[:hostname]
65
+ else
66
+ host[:final_master] = host[:hostname]
67
+ end
68
+ host[:levels] = levels
69
+ end
70
+
71
+ def find_master_slave
72
+ topology = @config.hosts.inject({}) { |accum, h|
73
+ hostname = h.query("show global variables where Variable_name='hostname'")[0][:Value]
74
+ hostname = hostname.split('.')[0]
75
+ status = h.slave_status
76
+
77
+ if status && status[:Master_User] != 'test'
78
+ master_host = status[:Master_Host]
79
+ end
80
+
81
+ master_host = short_name_for_host(master_host) if master_host
82
+
83
+ accum[hostname] = {:master => master_host, :hostname => hostname}
84
+ accum
85
+ }
86
+
87
+ # fill in :is_master
88
+ topology.each { |k, v|
89
+ master_top = topology[v[:master]]
90
+ if master_top
91
+ master_top[:is_master] = 1
92
+ end
93
+ }
94
+ topology
95
+ end
96
+
97
+ def short_name_for_host(h)
98
+ if h =~ /\d+.\d+.\d+.\d+/
99
+ h = ip_to_short_hostname(h)
100
+ end
101
+ h.split('.')[0]
102
+ end
103
+
104
+ def ip_to_short_hostname(ip)
105
+ `grep #{ip} /etc/hosts | awk '{print $2}'`.chomp
106
+ end
107
+ end
108
+
109
+ Filter.add_filter("discover_topology") do |queries, hostinfo, config|
110
+ if !config.options['discovered']
111
+ config.options['discovered'] = true
112
+ config.hosts = MMTop::Topology.new(config).new_hostlist
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ module MMTop
2
+ class ReverseLookup
3
+ def self.lookup(client)
4
+ return client if client.nil? or client.empty?
5
+ return client unless client =~ /\d+\.\d+\.\d+\.\d+/
6
+
7
+ @@lookups ||= {}
8
+
9
+ return @@lookups[client] if @@lookups[client]
10
+
11
+ hostline = %x{dig -x #{client} +short}.chomp
12
+ hostline.gsub!(/([^\.]+)\..*/, '\1')
13
+ @@lookups[client] = hostline
14
+ @@lookups[client]
15
+ end
16
+ end
17
+
18
+ Filter.add_filter("reverse_lookup") do |queries|
19
+ queries.each do |q|
20
+ q.client = MMTop::ReverseLookup.lookup(q.client)
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,88 @@
1
+ module MMTop
2
+ class Host
3
+ def initialize(hostname, user, password, options)
4
+ m2opts = {}
5
+ m2opts[:host] = hostname
6
+ m2opts[:username] = user
7
+ m2opts[:password] = password
8
+ m2opts[:socket] = options['socket'] if options['socket']
9
+ m2opts[:port] = options['port'] if options['port']
10
+ m2opts[:reconnect] = true
11
+ @mysql = Mysql2::Client.new(m2opts)
12
+ # rescue connection errors or sumpin
13
+ @options = options
14
+ @name = hostname
15
+ @display_name = @name
16
+ @comment = options['comment']
17
+ @last_queries = nil
18
+ end
19
+
20
+ attr_accessor :display_name, :name, :comment, :options
21
+
22
+ def query(q)
23
+ res = []
24
+ ret = @mysql.query(q)
25
+ return nil unless ret
26
+ ret.each(:symbolize_keys => true) do |r|
27
+ res << r
28
+ end
29
+ res
30
+ end
31
+
32
+ def slave_status
33
+ return nil if @options['expect_slave'] == false
34
+ res = query("show slave status")[0]
35
+ return nil if res && res[:Master_User] == 'test'
36
+ res
37
+ end
38
+
39
+ def wedge_monitor?
40
+ @options['wedge_monitor']
41
+ end
42
+
43
+ def stats
44
+ stats = {}
45
+ queries = query("show global status like 'Questions'")[0][:Value].to_i
46
+
47
+ if @last_queries
48
+ elapsed = Time.now.to_i - @last_queries[:time]
49
+ if elapsed > 0
50
+ qps = (queries - @last_queries[:count]) / elapsed
51
+ else
52
+ qps = queries - @last_queries[:count]
53
+ end
54
+ stats[:qps] = qps
55
+ end
56
+ @last_queries = {}
57
+ @last_queries[:count] = queries
58
+ @last_queries[:time] = Time.now.to_i
59
+ stats
60
+ end
61
+
62
+ def processlist
63
+ processlist = query("show full processlist")
64
+
65
+ processlist.map { |r| Process.new(r, self) }
66
+ end
67
+
68
+ def hostinfo
69
+ HostInfo.new(self, processlist, slave_status, stats)
70
+ end
71
+ end
72
+
73
+ class HostInfo
74
+ def initialize(host, processlist, slave_status, stats)
75
+ @host = host
76
+ @processlist = processlist
77
+ @connections = processlist.clone
78
+ @slave_status = slave_status
79
+ @stats = stats
80
+ end
81
+
82
+ attr_reader :host, :processlist, :slave_status, :stats
83
+
84
+ def connections
85
+ @connections
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,68 @@
1
+ module MMTop
2
+ class Config
3
+ def initialize(cmdline)
4
+ config_file = cmdline["-c"] || File.join(ENV["HOME"], ".mmtop_config")
5
+ raise "No file found: #{config_file}" unless File.exist?(config_file)
6
+ config = YAML.load_file(config_file)
7
+
8
+ if config['hosts'].nil? || config['hosts'].empty?
9
+ raise "Please configure the 'hosts' section of mmtop_config"
10
+ end
11
+
12
+ user = cmdline['-u']
13
+ pass = cmdline['-p']
14
+ @hosts = config['hosts'].map do |h|
15
+ h['user'] ||= (user || config['user'])
16
+ h['password'] ||= (pass || config['password'])
17
+ h['wedge_monitor'] ||= config['wedge_monitor']
18
+
19
+ begin
20
+ Host.new(h['host'], h['user'], h['password'], h)
21
+ rescue Mysql2::Error => e
22
+ puts e
23
+ nil
24
+ end
25
+ end.compact
26
+
27
+ @filters = MMTop::Filter.default_filters
28
+ config['sleep'] ||= 5
29
+ @options = config
30
+ end
31
+
32
+ attr_accessor :hosts
33
+ attr_accessor :info
34
+ attr_accessor :filters
35
+ attr_accessor :options
36
+
37
+ def find_pid(pid)
38
+ ret = info.map { |i|
39
+ i.processlist.detect { |p|
40
+ p.id == pid
41
+ }
42
+ }.flatten.compact
43
+
44
+ ret[0]
45
+ end
46
+
47
+ def find_server(name)
48
+ @info.detect { |i| i.host.name.downcase == name }
49
+ end
50
+
51
+ def run_filters
52
+ @info.each do |i|
53
+ @filters.each do |f|
54
+ f.run(i.processlist, i, self)
55
+ end
56
+ end
57
+ end
58
+
59
+ def all_processes
60
+ @info.map(&:processlist).flatten
61
+ end
62
+
63
+ def get_info
64
+ @info = hosts.map { |h| h.hostinfo }
65
+ run_filters
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ module MMTop
2
+ class PID
3
+ def self.get
4
+ @id += 1
5
+ end
6
+ def self.reset
7
+ @id = 0
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,34 @@
1
+ module MMTop
2
+ class Process
3
+ def initialize(result, host)
4
+ @real_id = result[:Id]
5
+ @query = result[:Info]
6
+ @status = result[:State]
7
+ @time = result[:Time]
8
+ @client, @client_port = result[:Host] && result[:Host].split(":")
9
+ @client ||= "(slave)"
10
+ @client_port ||= ""
11
+ @db = result[:db]
12
+ @host = host
13
+ end
14
+
15
+ attr_accessor :query, :status, :time, :client, :host, :db
16
+
17
+ def id
18
+ @id ||= MMTop::PID.get
19
+ end
20
+
21
+ def kill!
22
+ @host.query("KILL #{@real_id}")
23
+ end
24
+
25
+ def sql
26
+ @query
27
+ end
28
+
29
+ def explain
30
+ @host.query("use #{db}")
31
+ @host.query("explain #{sql}")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,53 @@
1
+ class String
2
+ CODES = {
3
+ :off => "\e[0m",
4
+ :bright => "\e[1m",
5
+ :underline => "\e[4m",
6
+ :blink => "\e[5m",
7
+ :swap => "\e[7m",
8
+ :hide => "\e[8m",
9
+
10
+ :black => "\e[30m",
11
+ :dark_gray => "\e[1;30m",
12
+ :red => "\e[31m",
13
+ :green => "\e[32m",
14
+ :yellow => "\e[33m",
15
+ :blue => "\e[34m",
16
+ :magenta => "\e[35m",
17
+ :cyan => "\e[36m",
18
+ :white => "\e[37m",
19
+ :default => "\e[39m",
20
+
21
+ :black_background => "\e[40m",
22
+ :red_background => "\e[41m",
23
+ :green_background => "\e[42m",
24
+ :yellow_background => "\e[43m",
25
+ :blue_background => "\e[44m",
26
+ :magenta_background => "\e[45m",
27
+ :cyan_background => "\e[46m",
28
+ :white_background => "\e[47m",
29
+ :default_background => "\e[49m"
30
+ }
31
+
32
+ OFF = CODES[:off]
33
+
34
+ def colorize(*args)
35
+ color = args.map { |color| CODES[color] if color.is_a?(Symbol) }.join("")
36
+ "#{color}#{self}#{OFF}"
37
+ end
38
+ def colourise(*args); colorize(*args); end
39
+
40
+ def red; colorize(:red); end
41
+ def green; colorize(:green); end
42
+ def bold; colorize(:bright); end
43
+ def black; colorize(:black); end
44
+ def white; colorize(:white); end
45
+ def dark_gray; colorize(:dark_gray); end
46
+
47
+ def size_uncolorized
48
+ self.gsub(/\e.*?m/, '').size_raw
49
+ end
50
+
51
+ alias :size_raw :size
52
+ alias :size :size_uncolorized
53
+ end
@@ -0,0 +1,59 @@
1
+ require 'timeout'
2
+ require 'readline'
3
+
4
+ module MMTop
5
+ class TermInput
6
+ def raw(bool)
7
+ if bool
8
+ %x{stty -echo raw}
9
+ else
10
+ %x{stty echo -raw}
11
+ end
12
+ end
13
+
14
+ def initialize()
15
+ end
16
+
17
+ def find_command(cmd)
18
+ MMTop::Command.commands.detect { |c| c.matches?(cmd) }
19
+ end
20
+
21
+ def control_mode(config)
22
+ raw(false)
23
+ while true
24
+ cmdline = Readline::readline('> ')
25
+ exit if cmdline.nil?
26
+ Readline::HISTORY.push(cmdline)
27
+ return if cmdline.empty?
28
+ c = find_command(cmdline)
29
+ if c.nil?
30
+ c = find_command("help")
31
+ end
32
+ c.run(cmdline, config)
33
+ end
34
+ end
35
+
36
+ def control(config)
37
+ raw(true)
38
+ char = nil
39
+ begin
40
+ Timeout::timeout(config.options['sleep']) do
41
+ char = $stdin.read(1)
42
+ end
43
+ rescue Timeout::Error
44
+ return
45
+ end
46
+
47
+ case char
48
+ when "\n"
49
+ return
50
+ when "p"
51
+ control_mode(config)
52
+ when "q"
53
+ exit(0)
54
+ end
55
+ ensure
56
+ raw(false)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,181 @@
1
+ require 'lib/string_colorize'
2
+
3
+ module MMTop
4
+ class TermPrinter
5
+ def initialize
6
+ $stdout.sync = true
7
+ end
8
+
9
+ def reset!
10
+ @header_columns = nil
11
+ get_dim
12
+ end
13
+
14
+ def get_dim
15
+ @y, @x = %x{stty size}.split.collect { |x| x.to_i }
16
+ end
17
+
18
+ def corner
19
+ "+".dark_gray
20
+ end
21
+
22
+ def edge
23
+ "-".dark_gray
24
+ end
25
+
26
+ def pipe
27
+ "|".dark_gray
28
+ end
29
+
30
+ def sep
31
+ " | ".dark_gray
32
+ end
33
+
34
+ def info_sep
35
+ " | ".dark_gray
36
+ end
37
+
38
+ def fill
39
+ "-".dark_gray
40
+ end
41
+
42
+ def print_header
43
+ puts corner + edge * (@x - 2) + corner
44
+ end
45
+
46
+ def print_footer
47
+ puts corner + edge * (@x - 2) + corner
48
+ end
49
+
50
+ def table_header_columns
51
+ @header_columns ||= ["hostname ", "pid ", "time", "#cx", "slave ", "delay", "qps ", "comment", Time.now.to_s]
52
+ end
53
+
54
+ def column_fill(index)
55
+ fill * table_header_columns[index].size
56
+ end
57
+
58
+ def sep_fill
59
+ fill * sep.size
60
+ end
61
+
62
+ def column_value(index, str, fill=" ")
63
+ if str.size < table_header_columns[index].size
64
+ str + (fill * (table_header_columns[index].size - str.size))
65
+ else
66
+ str[0..table_header_columns[index].size - 1]
67
+ end
68
+ end
69
+
70
+ def format_slave_status(status)
71
+ if status.nil? || status.keys.empty?
72
+ ""
73
+ else
74
+ if status[:Slave_IO_Running] == 'Yes' && status[:Slave_SQL_Running] == 'Yes'
75
+ "OK"
76
+ elsif status[:Slave_IO_Running] == 'No' && status[:Slave_SQL_Running] == 'No'
77
+ "STOPPED"
78
+ elsif status[:Slave_IO_Running] == 'Yes'
79
+ "!SQL"
80
+ else
81
+ "!IO"
82
+ end
83
+ end
84
+ end
85
+
86
+ def format_slave_delay(status)
87
+ return "" unless status
88
+ if status[:Seconds_Behind_Master].nil?
89
+ "N/A"
90
+ else
91
+ format_time(status[:Seconds_Behind_Master])
92
+ end
93
+ end
94
+
95
+ def format_time(x)
96
+ case x
97
+ when 0..100
98
+ x.to_s
99
+ when 100..3599
100
+ (x / 60).to_s + "m"
101
+ else
102
+ (x / 3600).to_s + "h"
103
+ end
104
+ end
105
+
106
+ def format_process(process, sz)
107
+ query = process.sql ? process.sql[0..sz-2] : ''
108
+ case process.time
109
+ when 0..2:
110
+ query
111
+ when 2..10:
112
+ query.white.bold
113
+ else
114
+ query.red
115
+ end
116
+ end
117
+
118
+ def clear_screen
119
+ print "\033[H\033[2J"
120
+ end
121
+
122
+ def print_process(p)
123
+ return if p.status.nil? || p.status.empty?
124
+ str = pipe + " " + column_value(0, " " + p.client)
125
+ str += info_sep + column_value(1, p.id ? p.id.to_s : '')
126
+ str += info_sep + column_value(2, format_time(p.time))
127
+ str += info_sep
128
+ str += format_process(p, @x - str.size - 1)
129
+ str += " " * (@x - str.size - 1) + pipe
130
+ puts str
131
+ end
132
+
133
+ def print_host(info)
134
+ str = pipe + " " + column_value(0, info.host.display_name + " " + (info.host.comment || ""), "-".dark_gray)
135
+ str += sep_fill + column_fill(1) + sep_fill + column_fill(2)
136
+ str += info_sep + column_value(3, info.connections.size.to_s)
137
+ str += info_sep + column_value(4, format_slave_status(info.slave_status))
138
+ str += info_sep + column_value(5, format_slave_delay(info.slave_status))
139
+ str += info_sep + column_value(6, info.stats[:qps].to_s)
140
+ #str += info_sep + column_value(7, info.host.comment || '')
141
+ str += info_sep
142
+ str += "-".dark_gray * (@x - str.size - 1)
143
+ str += pipe
144
+ puts str
145
+ info.processlist.each do |p|
146
+ print_process p
147
+ end
148
+ end
149
+
150
+ def print_table_header
151
+ if table_header_columns.join(sep).size + 4 > @x
152
+ table_header_columns[-1] = ''
153
+ end
154
+
155
+ str = pipe + " " + table_header_columns.join(sep)
156
+ fill_len = (@x - str.size) - 1
157
+
158
+ print str
159
+ print ' ' * fill_len if fill_len > 0
160
+ puts pipe
161
+ end
162
+
163
+ def print_info(host_infos)
164
+ #max_comment_size = host_infos.map { |i| (i.host.comment && i.host.comment.size).to_i }.max
165
+ #comment_index = 7
166
+ #table_header_columns[comment_index] += (' ' * (max_comment_size - table_header_columns[comment_index].size)) if max_comment_size > table_header_columns[comment_index].size
167
+
168
+ clear_screen
169
+ reset!
170
+ print_header
171
+ print_table_header
172
+ print_header
173
+
174
+
175
+ host_infos.each do |info|
176
+ print_host(info)
177
+ end
178
+ print_footer
179
+ end
180
+ end
181
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mmtop
3
+ version: !ruby/object:Gem::Version
4
+ hash: 59
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 9
9
+ - 0
10
+ version: 0.9.0
11
+ platform: ruby
12
+ authors:
13
+ - Ben Osheroff
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-12-09 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mysql2
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: ruby-debug
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description:
50
+ email:
51
+ - ben@gimbo.net
52
+ executables:
53
+ - mmtop
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - lib/command.rb
60
+ - lib/commands/basic.rb
61
+ - lib/commands/filters.rb
62
+ - lib/commands/kill.rb
63
+ - lib/filter.rb
64
+ - lib/filters/basic.rb
65
+ - lib/filters/map_topology.rb
66
+ - lib/filters/reverse_lookup.rb
67
+ - lib/host.rb
68
+ - lib/mmconfig.rb
69
+ - lib/pid.rb
70
+ - lib/process.rb
71
+ - lib/string_colorize.rb
72
+ - lib/term_input.rb
73
+ - lib/term_printer.rb
74
+ - bin/mmtop
75
+ has_rdoc: true
76
+ homepage: http://github.com/osheroff/mmtop
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ requirements: []
103
+
104
+ rubyforge_project:
105
+ rubygems_version: 1.5.3
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: A mytop-ish variant that can watch many mysql servers
109
+ test_files: []
110
+