reptile 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,6 +6,52 @@ Reptile is an easy to use utility that will monitor your MySQL replication, so y
6
6
 
7
7
  A mysql replication setup.
8
8
 
9
+ == SYNOPSIS
10
+
11
+ Put a config file at /etc/reptile.yml, /etc/reptile/reptile.yml, ./reptile.yml, or specify one on the command line.
12
+
13
+
14
+ [nick@s34 ~]$ replication_status -h
15
+ Usage: replication_status [path_to_config_file]
16
+ -h, --help Displays this help info
17
+ -s, --status Displays the slave status
18
+ -d, --diff Checks the row count difference between master and slave
19
+ -r, --report Sends a report email
20
+ -b, --heartbeat Checks the heartbeat timestamp difference between master and slave
21
+ [nick@s34 ~]$ replication_status -s
22
+ * a_database slave is up and running
23
+ * b_database slave is up and running
24
+ * c_database slave is up and running
25
+
26
+ === Super simple crontab setup
27
+
28
+ [nick@s34 ~]$ sudo crontab -l
29
+ Password:
30
+ MAILTO="user@example.com"
31
+ # Use the '-e' arg to only spit out errors, which will be sent out by crontab
32
+ */15 * * * * replication_status -e
33
+
34
+ === Setup SQL
35
+
36
+ Run this SQL to setup the DBs/perms for Reptile. Using different users, since each user requires significantly different permissions.
37
+
38
+ GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'repl'@"localhost" IDENTIFIED BY 'repl_user_pass';
39
+ GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'repl'@"MONITOR_HOST_IP" IDENTIFIED BY 'repl_user_pass';
40
+
41
+ GRANT SELECT, REPLICATION CLIENT ON *.* TO 'ro_user'@"localhost" IDENTIFIED BY 'ro_user_pass';
42
+ GRANT SELECT, REPLICATION CLIENT ON *.* TO 'ro_user'@"MONITOR_HOST_IP" IDENTIFIED BY 'ro_user_pass';
43
+
44
+ CREATE DATABASE replication_monitor;
45
+
46
+ GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'heartbeat_user'@"localhost" IDENTIFIED BY 'heartbeat_user_pass';
47
+ GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'heartbeat_user'@"MONITOR_HOST_IP" IDENTIFIED BY 'heartbeat_user_pass';
48
+
49
+ CREATE TABLE replication_monitor.heartbeats (
50
+ unix_time INTEGER NOT NULL,
51
+ db_time TIMESTAMP NOT NULL,
52
+ INDEX time_idx(unix_time)
53
+ )
54
+
9
55
  == INSTALL:
10
56
 
11
57
  sudo gem install reptile
@@ -33,4 +79,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
33
79
  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
34
80
  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
35
81
  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
36
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
82
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -5,11 +5,12 @@ require File.dirname(__FILE__) + '/lib/reptile'
5
5
  # Generate all the Rake tasks
6
6
  # Run 'rake -T' to see list of generated tasks (from gem root directory)
7
7
  $hoe = Hoe.new('reptile', Reptile::VERSION) do |p|
8
- p.developer('FIXME full name', 'FIXME email')
8
+ p.developer('Nick Stielau', 'nick.stielau@gmail.com')
9
9
  p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
10
10
  p.post_install_message = 'PostInstall.txt'
11
11
  p.rubyforge_name = p.name
12
12
  p.bin_files = ["bin/replication_status"]
13
+ p.summary = "Cold-blooded MySQL replication monitoring."
13
14
  p.extra_dev_deps = [
14
15
  ['newgem', ">= #{::Newgem::VERSION}"]
15
16
  ]
@@ -22,13 +23,13 @@ end
22
23
 
23
24
  desc "Upload current documentation to Rubyforge"
24
25
  task :upload_docs => [:redocs] do
25
- sh "scp -r doc/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/doc/"
26
+ sh "scp -r doc/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/doc"
26
27
  end
27
28
 
28
29
  desc "Upload current documentation to Rubyforge"
29
30
  task :upload_site do
30
31
  #webgen && scp -r output/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/
31
- sh "scp -r site/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/"
32
+ sh "scp -r webgen_site/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/"
32
33
  end
33
34
 
34
35
  require 'newgem/tasks' # load /tasks/*.rake
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
3
4
  require 'optparse'
4
5
  require 'reptile'
5
6
 
@@ -36,6 +37,10 @@ OptionParser.new do |opts|
36
37
  opts.on("-g", "--start_slaves", "Starts all slaves") do
37
38
  commands << 'start_slaves'
38
39
  end
40
+
41
+ opts.on("-e", "--errors", "Show only errors.") do
42
+ commands << 'errors'
43
+ end
39
44
 
40
45
  begin
41
46
  opts.parse!(ARGV)
@@ -46,19 +51,37 @@ OptionParser.new do |opts|
46
51
  end
47
52
  end
48
53
 
49
- config_file = 'replication.yml'
54
+ if !ARGV.empty? && ARGV.length > 1
55
+ abort "Too many arguments; please specify only the directory containing the 'reptile.yml' file, or a yml config file itself."
56
+ end
50
57
 
51
- if ARGV.empty? && !File.exists?(config_file)
52
- abort "Please specify the directory containing the '#{config_file}' file, e.g. `#{File.basename($0)} ~/repl'"
53
- elsif !ARGV.empty? && !File.exists?(ARGV.first)
54
- abort "`#{ARGV.first}' does not exist."
55
- elsif !ARGV.empty? && !File.directory?(ARGV.first)
56
- abort "`#{ARGV.first}' is not a directory."
57
- elsif !ARGV.empty? && ARGV.length > 1
58
- abort "Too many arguments; please specify only the directory to #{File.basename($0)}."
58
+ config_file_name = "reptile.yml"
59
+ config_file_locations = ["/etc/#{config_file_name}", "/etc/reptile/#{config_file_name}", "./#{config_file_name}"]
60
+ config_location_param = ARGV.first
61
+
62
+ config_file = nil
63
+ if config_location_param
64
+ if File.directory?(config_location_param) && File.exist?(File.join(config_location_param, config_file_name))
65
+ config_file = File.join(config_location_param, config_file_name)
66
+ elsif File.exist?(config_location_param) && !File.directory?(config_location_param)
67
+ config_file = config_location_param
68
+ else
69
+ abort "Please specify the directory containing the '#{config_file_name}' file, or the config file itself."
70
+ end
71
+ else
72
+ config_file_locations.each do |f|
73
+ if File.exist?(f)
74
+ config_file = f
75
+ break
76
+ end
77
+ end
78
+ if config_file.nil?
79
+ abort "Couldn't find a config file at #{config_file_locations.join(', ')}"
80
+ end
59
81
  end
60
82
 
61
- Reptile::ReplicationMonitor.load_config_file(ARGV.first.nil? ? config_file : "#{ARGV.first}/#{config_file}")
83
+
84
+ Reptile::ReplicationMonitor.load_config_file(config_file)
62
85
 
63
86
  if (commands.include?('start_slaves') || commands.include?('stop_slaves'))
64
87
  Reptile::Runner.send(commands[0])
@@ -22,15 +22,15 @@ private
22
22
 
23
23
  def get_masters
24
24
  masters = databases.dup
25
- masters.each_key{|key| masters.delete(key) if masters[key]['master'].nil? }
26
- masters.each_key{|key| masters[key] = masters[key]['master'] }
25
+ masters.each_key{|name| masters.delete(name) if masters[name]['master'].nil? }
26
+ masters.each_key{|name| masters[name] = masters[name]['master'] }
27
27
  masters
28
28
  end
29
29
 
30
30
  # TODO: make private
31
31
  def get_slaves
32
32
  dbs = databases.dup
33
- dbs.each_key{|name| dbs.delete(key) if dbs[name]['slave'].nil? }
33
+ dbs.each_key{|name| dbs.delete(name) if dbs[name]['slave'].nil? }
34
34
  dbs.each_key{|name| dbs[name] = dbs[name]['slave'] }
35
35
  slaves = dbs
36
36
  end
@@ -44,7 +44,7 @@ private
44
44
  puts "****"
45
45
  puts "Error connecting to database: #{e}"
46
46
  puts "****"
47
- puts YAML::dump(configs)
47
+ puts configs.inspect
48
48
  exit 1
49
49
  end
50
50
  end
@@ -1,3 +1,5 @@
1
+ require 'logger'
2
+
1
3
  module Reptile
2
4
  # This monitor compares the row counts for each table for each master and slave.
3
5
  class DeltaMonitor
@@ -12,15 +14,12 @@ module Reptile
12
14
  @user
13
15
  end
14
16
 
15
- def self.open_log()
16
- logFile = 'db_delta.log'
17
- @logFileObj = File.open(logFile, "a")
17
+ def self.logger=(new_logger)
18
+ @logger = new_logger
18
19
  end
19
-
20
- def self.log(msg)
21
- open_log if @logFileObj.nil?
22
- puts msg
23
- @logFileObj.puts msg
20
+
21
+ def self.get_logger
22
+ @logger ||= Logger.new(STDOUT)
24
23
  end
25
24
 
26
25
  # Retrieve the active database connection. Nil of none exists.
@@ -46,18 +45,24 @@ module Reptile
46
45
  print_deltas(db_name, deltas, master_configs)
47
46
 
48
47
  deltas
48
+ rescue Exception => e
49
+ get_logger.error "Error: Caught #{e}"
50
+ get_logger.error "DB Name: #{db_name}"
51
+ get_logger.error "Master Configs: #{master_configs.inspect}"
52
+ get_logger.error "Slave Configs: #{slave_configs.inspect}"
53
+ raise e
49
54
  end
50
55
 
51
56
  # Prints stats about the differences in number of rows between the master and slave
52
57
  def self.print_deltas(db_name, deltas, configs)
53
58
  non_zero_deltas = deltas.select{|table, delta| not delta.zero?}
54
59
  if non_zero_deltas.size.zero?
55
- log "Replication counts A-OK for #{db_name} on #{configs['host']} @ #{Time.now}"
60
+ get_logger.info "Replication counts A-OK for #{db_name} on #{configs['host']} @ #{Time.now}"
56
61
  else
57
- log "Replication Row Count Deltas for #{db_name} on #{configs['host']} @ #{Time.now}"
58
- log "There #{non_zero_deltas.size > 1 ? 'are' : 'is'} #{non_zero_deltas.size} #{non_zero_deltas.size > 1 ? 'deltas' : 'delta'}"
62
+ get_logger.info "Replication Row Count Deltas for #{db_name} on #{configs['host']} @ #{Time.now}"
63
+ get_logger.info "There #{non_zero_deltas.size > 1 ? 'are' : 'is'} #{non_zero_deltas.size} #{non_zero_deltas.size > 1 ? 'deltas' : 'delta'}"
59
64
  non_zero_deltas.each do |table, delta|
60
- log " #{table} table: #{delta}" unless delta.zero?
65
+ get_logger.info " #{table} table: #{delta}" unless delta.zero?
61
66
  end
62
67
  end
63
68
  end
@@ -28,23 +28,22 @@ module Reptile
28
28
  def self.user
29
29
  @user ||= {}
30
30
  end
31
-
32
- # Merge the connection settings in the configs parameter with HeartBeat defaults.
31
+
33
32
  def self.connect(configs)
34
- ReplicationMonitor.connect(configs.merge(user))
33
+ Databases.connect(configs.merge(user).merge("database" => 'replication_monitor'))
35
34
  end
36
35
 
37
36
  # Write a heartbeat.
38
37
  # Thump thump.
39
38
  def self.write(name, configs)
40
- connect(configs)
39
+ self.connect(configs)
41
40
  heartbeat = Heartbeat.create(:unix_time => Time.now.to_i, :db_time => "NOW()")
42
- log "Wrote heartbeat to #{name} at #{Time.at(heartbeat.unix_time)}"
41
+ get_logger.info "Wrote heartbeat to #{name} at #{Time.at(heartbeat.unix_time)}"
43
42
  end
44
43
 
45
44
  # Read the most recent heartbeat and return the delay in seconds, or nil if no heartbeat are found.
46
45
  def self.read(name, configs)
47
- connect(configs)
46
+ self.connect(configs)
48
47
 
49
48
  current_time = Time.now
50
49
 
@@ -53,7 +52,7 @@ module Reptile
53
52
 
54
53
  # No heartbeats at all!
55
54
  if heartbeat.nil?
56
- log "No heartbeats found on #{name} at #{Time.now}"
55
+ get_logger.info "No heartbeats found on #{name} at #{Time.now}"
57
56
  return nil;
58
57
  end
59
58
 
@@ -62,26 +61,21 @@ module Reptile
62
61
  delay = (Time.now - Time.at(heartbeat.unix_time)).round
63
62
  #delay = (Time.now - heartbeat.db_time)
64
63
 
65
- log "Read heartbeat from #{name} at #{Time.at(heartbeat.unix_time)}. The delay is #{strfdelay(delay)}"
64
+ get_logger.info "Read heartbeat from #{name} at #{Time.at(heartbeat.unix_time)}. The delay is #{strfdelay(delay)}"
66
65
 
67
66
  delay
68
67
  end
69
68
 
70
- private
71
-
72
- # Open the 'heartbeat.log' file.
73
- def self.open_log
74
- logFile = 'heartbeat.log'
75
- @logFileObj = File.open(logFile, "a")
69
+ def self.logger=(new_logger)
70
+ @logger = new_logger
76
71
  end
77
-
78
- # Log a message, both to the file and standard out.
79
- def self.log(msg)
80
- open_log if @logFileObj.nil?
81
- puts msg
82
- @logFileObj.puts msg
72
+
73
+ def self.get_logger
74
+ @logger ||= Logger.new(STDOUT)
83
75
  end
84
76
 
77
+ private
78
+
85
79
  # Format the delay (in seconds) as a human-readable string.
86
80
  def self.strfdelay(delay)
87
81
  seconds = delay % 60
@@ -1,7 +1,10 @@
1
+ require 'logger'
2
+
1
3
  module Reptile
2
4
  class ReplicationMonitor
5
+
3
6
  # Attempts to load the replication.yml configuration file.
4
- def self.load_config_file(databases_file)
7
+ def self.load_config_file(databases_file)
5
8
  @databases_file = databases_file
6
9
  yaml = YAML::load(File.read(@databases_file))
7
10
  @configs = yaml.delete('config')
@@ -14,8 +17,8 @@ module Reptile
14
17
  DeltaMonitor.user = users.ro_user
15
18
  Runner.databases = databases
16
19
 
17
- raise "Please specify a delay theshold 'delay_threshold_secs: 360'" if @configs['delay_threshold_secs'].nil?
18
- raise "Please specify a row delta theshold 'row_difference_threshold: 10'" if @configs['row_difference_threshold'].nil?
20
+ raise "Please specify a delay threshold 'delay_threshold_secs: 360'" if @configs['delay_threshold_secs'].nil?
21
+ raise "Please specify a row delta threshold 'row_difference_threshold: 10'" if @configs['row_difference_threshold'].nil?
19
22
 
20
23
  rescue Errno::EACCES => e
21
24
  puts "Unable to open config file: Permission Denied"
@@ -35,17 +38,25 @@ module Reptile
35
38
  def self.users
36
39
  @users
37
40
  end
41
+
42
+ def self.errors
43
+ get_logger.sev_threshold = Logger::ERROR
44
+ check_slaves
45
+ heartbeat
46
+ diff_tables
47
+ end
38
48
 
39
49
  def self.diff_tables
40
50
  unsynced_dbs = 0
51
+ DeltaMonitor.logger = get_logger
41
52
 
42
- databases.each_pair do |name, roles|
53
+ databases.databases.each_pair do |name, roles|
43
54
  master, slave = roles['master'], roles['slave']
44
55
  deltas = DeltaMonitor.diff(name, master, slave)
45
56
 
46
57
  egregious_deltas = deltas.select{|table, delta| delta > configs['row_difference_threshold'] }
47
58
  if egregious_deltas.size > 0
48
- queue_replication_warning :host => master[:host], :database => master[:database], :deltas => egregious_deltas, :noticed_at => Time.now
59
+ queue_replication_warning :host => master["host"], :database => master["database"], :deltas => egregious_deltas, :noticed_at => Time.now
49
60
  unsynced_dbs += 1
50
61
  end
51
62
  end
@@ -54,14 +65,15 @@ module Reptile
54
65
  end
55
66
 
56
67
  def self.heartbeat
57
- databases.each_key do |name|
58
- Heartbeat.write(name, databases[name]['master'])
68
+ Heartbeat.logger = get_logger
69
+
70
+ databases.masters.each_pair do |name, configs|
71
+ Heartbeat.write(name, configs)
59
72
  end
60
73
 
61
74
  overdue_slaves = 0
62
75
 
63
- databases.each_key do |name|
64
- db_configs = databases[name]['slave']
76
+ databases.slaves.each_pair do |name, db_configs|
65
77
  delay = Heartbeat.read(name, db_configs)
66
78
  if delay.nil?
67
79
  queue_replication_warning :host => name,
@@ -85,8 +97,9 @@ module Reptile
85
97
  def self.check_slaves
86
98
  databases.slaves.each do |slave_name, slave_configs|
87
99
  status = Status.check_slave_status(slave_name, slave_configs)
100
+ get_logger.info "'#{slave_name}' is '#{status}'"
88
101
  if status != Status.const_get(:RUNNING)
89
- queue_replication_warning :host => name,
102
+ queue_replication_warning :host => slave_name,
90
103
  :database => configs[:database],
91
104
  :status_error => Status.get_error_message(status),
92
105
  :noticed_at => Time.now
@@ -101,7 +114,7 @@ module Reptile
101
114
  email.body = ''
102
115
 
103
116
  if options[:delay]
104
- email.body += "There was a #{delay} second replication latency, which is greater than the allowed latency of #{configs['delay_threshold_secs']} seconds"
117
+ email.body += "There was a #{options[:delay]} second replication latency, which is greater than the allowed latency of #{configs['delay_threshold_secs']} seconds"
105
118
  elsif options[:deltas]
106
119
  email.body += "The following tables have master/slave row count difference greater than the allowed #{configs['row_difference_threshold']}\n\n"
107
120
  options[:deltas].each do |table, delta|
@@ -115,8 +128,11 @@ module Reptile
115
128
 
116
129
  email.body += "\n"
117
130
  email.body += " Server: #{options[:host]}\n"
118
- email.body += " Databse: #{options[:database]}\n"
131
+ email.body += " Database: #{options[:database]}\n" unless options[:database].blank?
119
132
 
133
+ # Print out email body to STDOUT
134
+ get_logger.error email.body
135
+
120
136
  send_email(email)
121
137
  end
122
138
 
@@ -189,6 +205,9 @@ module Reptile
189
205
  end
190
206
 
191
207
  def self.send_email(email)
208
+ return unless configs['email_server'] && configs['email_port'] && configs['email_domain'] &&
209
+ configs['email_password'] && configs['email_auth_type']
210
+
192
211
  # TODO: could do Net::SMTP.respond_to?(enable_tls) ? enable_TLS : puts "Install TLS gem to use SSL/TLS"
193
212
  Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
194
213
  Net::SMTP.start(configs['email_server'],
@@ -220,5 +239,11 @@ module Reptile
220
239
  # end
221
240
  end
222
241
 
242
+ private
243
+
244
+ def self.get_logger
245
+ @@logger ||= Logger.new(STDOUT)
246
+ end
247
+
223
248
  end
224
249
  end
@@ -29,7 +29,7 @@ module Reptile
29
29
  configs.delete('port')
30
30
  configs.delete('host')
31
31
  # With activeRecord, you have to connect to some DB, even if you are acting on the server...
32
- configs['database'] = 'information_schema'
32
+ configs['database'] = 'information_schema' unless configs['database']
33
33
  # TODO: Delete these somewhere else
34
34
  configs.delete('heartbeat')
35
35
  configs.delete('replication_user')
@@ -29,8 +29,7 @@ module Reptile
29
29
 
30
30
  # The user settings for a user that has global select privilidgess
31
31
  def self.user
32
- raise "You need to specify a user!" if @user.nil?
33
- @user
32
+ @user ||= {}
34
33
  end
35
34
 
36
35
  # Checks the value of the MySQL command "SHOW SLAVE STATUS".
@@ -38,13 +37,13 @@ module Reptile
38
37
  def self.check_slave_status(name, configs)
39
38
  # TODO: Do this in Databases
40
39
  configs.delete("port")
41
- configs['database'] = "information_schema"
42
- Databases.connect(configs.merge(user)).execute('SHOW SLAVE STATUS').each do |row|
43
- if row =~ /Slave_SQL_Running/ && row =~ /No/
40
+ Databases.connect(configs.merge(user).merge('database' => 'information_schema')).execute('SHOW SLAVE STATUS').each_hash do |hash|
41
+
42
+ if hash['Slave_SQL_Running'] == "No"
44
43
  return SQL_THREAD_DOWN
45
- elsif row =~ /Slave_IO_Running/ && row =~ /No/
44
+ elsif hash['Slave_IO_Running'] == "No"
46
45
  return IO_THREAD_DOWN
47
- elsif row =~ /Slave_Running/ && row =~ /No/
46
+ elsif hash['Slave_Running'] == "No"
48
47
  return SLAVE_DOWN
49
48
  else
50
49
  return RUNNING
data/lib/reptile/users.rb CHANGED
@@ -8,6 +8,8 @@ module Reptile
8
8
  # to be even easier.
9
9
  class Users
10
10
  def initialize(options)
11
+ return if options.nil?
12
+
11
13
  @repl_user = options["replication_user"]
12
14
  @ro_user = options["ro_user"]
13
15
  @heartbeat_user = options["heartbeat_user"]
data/lib/reptile.rb CHANGED
@@ -19,5 +19,5 @@ require 'reptile/databases'
19
19
  require 'active_record'
20
20
 
21
21
  module Reptile
22
- VERSION = '0.0.1'
22
+ VERSION = '0.0.4'
23
23
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reptile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
- - FIXME full name
7
+ - Nick Stielau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-26 00:00:00 -07:00
13
- default_executable:
12
+ date: 2009-08-13 00:00:00 -07:00
13
+ default_executable: replication_status
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: newgem
@@ -34,7 +34,7 @@ dependencies:
34
34
  version:
35
35
  description: ""
36
36
  email:
37
- - FIXME email
37
+ - nick.stielau@gmail.com
38
38
  executables:
39
39
  - replication_status
40
40
  extensions: []
@@ -66,7 +66,9 @@ files:
66
66
  - test/test_helper.rb
67
67
  - test/test_reptile.rb
68
68
  has_rdoc: true
69
- homepage: Reptile is an easy to use utility that will monitor your MySQL replication, so you can forget about it and focus on the good stuff. It provides a utility for generate replication reports, and can email if replication appears to be failing.
69
+ homepage: http://reptile.rubyforge.org/
70
+ licenses: []
71
+
70
72
  post_install_message: PostInstall.txt
71
73
  rdoc_options:
72
74
  - --main
@@ -88,10 +90,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
90
  requirements: []
89
91
 
90
92
  rubyforge_project: reptile
91
- rubygems_version: 1.3.1
93
+ rubygems_version: 1.3.5
92
94
  signing_key:
93
95
  specification_version: 2
94
- summary: ""
96
+ summary: Cold-blooded MySQL replication monitoring.
95
97
  test_files:
96
98
  - test/test_helper.rb
97
99
  - test/test_reptile.rb