mmtop 0.9.0

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.
@@ -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
+