mmtop 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/mmtop +43 -0
- data/lib/command.rb +45 -0
- data/lib/commands/basic.rb +96 -0
- data/lib/commands/filters.rb +73 -0
- data/lib/commands/kill.rb +75 -0
- data/lib/filter.rb +43 -0
- data/lib/filters/basic.rb +31 -0
- data/lib/filters/map_topology.rb +115 -0
- data/lib/filters/reverse_lookup.rb +24 -0
- data/lib/host.rb +88 -0
- data/lib/mmconfig.rb +68 -0
- data/lib/pid.rb +11 -0
- data/lib/process.rb +34 -0
- data/lib/string_colorize.rb +53 -0
- data/lib/term_input.rb +59 -0
- data/lib/term_printer.rb +181 -0
- metadata +110 -0
data/bin/mmtop
ADDED
@@ -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
|
+
|
data/lib/command.rb
ADDED
@@ -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
|
+
|
data/lib/filter.rb
ADDED
@@ -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
|
+
|
data/lib/host.rb
ADDED
@@ -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
|
data/lib/mmconfig.rb
ADDED
@@ -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
|
data/lib/pid.rb
ADDED
data/lib/process.rb
ADDED
@@ -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
|
data/lib/term_input.rb
ADDED
@@ -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
|
data/lib/term_printer.rb
ADDED
@@ -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
|
+
|