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