mysql_health 0.5.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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mysql_health.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 osterman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # MysqlHealth
2
+
3
+ MySQL Health is a standalone HTTP server that will respond with a 200 status code when MySQL is operating as expected.
4
+
5
+ This script is intended to be used in conjuction with HAProxy "option httpchk" for a TCP load balancer distributing load across mysql servers.
6
+
7
+ ## FAQs
8
+
9
+ 1. If you get the error "caught DBI::InterfaceError exception 'Could not load driver (uninitialized constant MysqlError)'" on OSX, try doing this:
10
+ `export DYLD_LIBRARY_PATH="/usr/local/mysql/lib:$DYLD_LIBRARY_PATH"`
11
+
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'mysql_health'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install mysql_health
26
+
27
+ ## Usage
28
+
29
+ Usage: mysql_health options
30
+ --check:master Master health check
31
+ --check:slave Slave health check
32
+ --check:allow-overlapping Allow overlapping health checks (default: false)
33
+ --check:interval INTERVAL Check health every INTERVAL (default: 10s)
34
+ --check:delay DELAY Delay health checks for INTERVAL (default: 0s)
35
+ --check:dsn DSN MySQL DSN (default: DBI:Mysql:mysql:localhost)
36
+ --check:username USERNAME MySQL Username (default: root)
37
+ --check:password PASSWORD MySQL Password (default: )
38
+ -l, --server:listen ADDR Server listen address (default: 0.0.0.0)
39
+ -p, --server:port PORT Server listen port (default: 3305)
40
+ -d, --server:daemonize Daemonize the process (default: false)
41
+ -P, --server:pid-file PID-FILE Pid-File to save the process id (default: false)
42
+ --log:level LEVEL Logging level (default: INFO)
43
+ --log:file FILE Write logs to FILE (default: STDERR)
44
+ --log:age DAYS Rotate logs after DAYS pass (default: 7)
45
+ --log:size SIZE Rotate logs after the grow past SIZE bytes (default: 10485760)
46
+
47
+ ## Examples
48
+
49
+ Start the server on port 1234 and check the status of the slave every 30 seconds:
50
+
51
+ mysql_health --check:slave --check:interval 30 --server:port 1234
52
+
53
+ Start the server on port 1234 and check the status of the master every 30 seconds:
54
+
55
+ mysql_health --check:master --check:interval 30 --server:port 1234
56
+
57
+ ## Contributing
58
+
59
+ 1. Fork it
60
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
61
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
62
+ 4. Push to the branch (`git push origin my-new-feature`)
63
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/mysql_health ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
4
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
5
+ #
6
+ # This file is part of mysql_health.
7
+ #
8
+ # mysql_health is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # mysql_health is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
20
+ #
21
+
22
+ $:.unshift(File.expand_path('.')) # Ruby 1.9 doesn't have . in the load path...
23
+ $:.push(File.expand_path('lib/'))
24
+
25
+ require 'rubygems'
26
+ require 'mysql_health'
27
+
28
+ command_line = MysqlHealth::CommandLine.new
29
+ command_line.execute
30
+
@@ -0,0 +1,27 @@
1
+ #
2
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of mysql_health.
6
+ #
7
+ # mysql_health is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # mysql_health is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'mysql_health/version'
21
+ require 'mysql_health/health'
22
+ require 'mysql_health/server'
23
+ require 'mysql_health/command_line'
24
+
25
+ module MysqlHealth
26
+
27
+ end
@@ -0,0 +1,199 @@
1
+ #
2
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of mysql_health.
6
+ #
7
+ # mysql_health is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # mysql_health is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'logger'
21
+ require 'optparse'
22
+
23
+ module MysqlHealth
24
+ @@health = nil
25
+ @@log = nil
26
+
27
+ def self.health
28
+ @@health
29
+ end
30
+
31
+ def self.health=(health)
32
+ @@health = health
33
+ end
34
+
35
+ def self.log
36
+ @@log
37
+ end
38
+
39
+ def self.log=(log)
40
+ @@log = log
41
+ end
42
+
43
+ class ArgumentException < Exception; end
44
+ class CommandLine
45
+ attr_accessor :options
46
+ def initialize
47
+ @options = {}
48
+ @options[:server] = {}
49
+ @options[:check] = {}
50
+ @options[:log] = {}
51
+
52
+ begin
53
+ @optparse = OptionParser.new do |opts|
54
+ opts.banner = "Usage: #{$0} options"
55
+ #
56
+ # Health check
57
+ #
58
+ @options[:check][:master] = false
59
+ opts.on( '--check:master', 'Master health check') do
60
+ @options[:check][:master] = true
61
+ end
62
+
63
+ @options[:check][:slave] = false
64
+ opts.on( '--check:slave', 'Slave health check') do |host|
65
+ @options[:check][:slave] = true
66
+ end
67
+
68
+ @options[:check][:allow_overlapping] = false
69
+ opts.on( '--check:allow-overlapping', "Allow overlapping health checks (default: #{@options[:check][:allow_overlapping]})") do
70
+ @options[:check][:allow_overlapping] = true
71
+ end
72
+
73
+ @options[:check][:interval] = '10s'
74
+ opts.on( '--check:interval INTERVAL', "Check health every INTERVAL (default: #{@options[:check][:interval]})") do |interval|
75
+ @options[:check][:interval] = interval.to_s
76
+ end
77
+
78
+ @options[:check][:delay] = '0s'
79
+ opts.on( '--check:delay DELAY', "Delay health checks for INTERVAL (default: #{@options[:check][:delay]})") do |delay|
80
+ @options[:check][:delay] = interval.to_s
81
+ end
82
+
83
+ @options[:check][:dsn] ||= "DBI:Mysql:mysql:localhost"
84
+ opts.on( '--check:dsn DSN', "MySQL DSN (default: #{@options[:check][:dsn]})") do |dsn|
85
+ @options[:check][:dsn] = dsn.to_s
86
+ end
87
+
88
+ @options[:check][:username] ||= "root"
89
+ opts.on( '--check:username USERNAME', "MySQL Username (default: #{@options[:check][:username]})") do |username|
90
+ @options[:check][:username] = username.to_s
91
+ end
92
+
93
+ @options[:check][:password] ||= ""
94
+ opts.on( '--check:password PASSWORD', "MySQL Password (default: #{@options[:check][:password]})") do |password|
95
+ @options[:check][:password] = interval.to_s
96
+ end
97
+
98
+ # Server
99
+ @options[:server][:listen] = '0.0.0.0'
100
+ opts.on( '-l', '--server:listen ADDR', "Server listen address (default: #{@options[:server][:listen]})") do |addr|
101
+ @options[:server][:addr] = host.to_s
102
+ end
103
+
104
+ @options[:server][:port] = 3305
105
+ opts.on( '-p', '--server:port PORT', "Server listen port (default: #{@options[:server][:port]})") do |port|
106
+ @options[:server][:port] = port.to_i
107
+ end
108
+
109
+ @options[:server][:daemonize] = false
110
+ opts.on( '-d', '--server:daemonize', "Daemonize the process (default: #{@options[:server][:daemonize]})") do
111
+ @options[:server][:daemonize] = true
112
+ end
113
+
114
+ @options[:server][:pid_file] = false
115
+ opts.on('-P', '--server:pid-file PID-FILE', "Pid-File to save the process id (default: #{@options[:server][:pid_file]})") do |pid_file|
116
+ @options[:server][:pid_file] = pid_file
117
+ end
118
+
119
+
120
+ #
121
+ # Logging
122
+ #
123
+
124
+ @options[:log][:level] = Logger::INFO
125
+ opts.on( '--log:level LEVEL', 'Logging level (default: INFO)' ) do|level|
126
+ @options[:log][:level] = Logger.const_get level.upcase
127
+ end
128
+
129
+ @options[:log][:file] = STDERR
130
+ opts.on( '--log:file FILE', 'Write logs to FILE (default: STDERR)' ) do|file|
131
+ @options[:log][:file] = File.open(file, File::WRONLY | File::APPEND | File::CREAT)
132
+ end
133
+
134
+ @options[:log][:age] = 7
135
+ opts.on( '--log:age DAYS', "Rotate logs after DAYS pass (default: #{@options[:log][:age]})" ) do|days|
136
+ @options[:log][:age] = days.to_i
137
+ end
138
+
139
+ @options[:log][:size] = 1024*1024*10
140
+ opts.on( '--log:size SIZE', "Rotate logs after the grow past SIZE bytes (default: #{@options[:log][:size]})" ) do |size|
141
+ @options[:log][:size] = size.to_i
142
+ end
143
+ end
144
+ @optparse.parse!
145
+
146
+ raise ArgumentException.new("No action specified") if @options[:check][:master] == false && @options[:check][:slave] == false
147
+ @log = Logger.new(@options[:log][:file], @options[:log][:age], @options[:log][:size])
148
+ @log.level = @options[:log][:level]
149
+
150
+ daemonize if @options[:server][:daemonize]
151
+ write_pid_file if @options[:server][:pid_file]
152
+
153
+ MysqlHealth.log = @log
154
+ MysqlHealth.health = Health.new(@options[:check])
155
+
156
+ rescue ArgumentException => e
157
+ puts e.message
158
+ puts @optparse
159
+ exit 1
160
+ end
161
+ end
162
+
163
+ def daemonize
164
+ # Become a daemon
165
+ if RUBY_VERSION < "1.9"
166
+ exit if fork
167
+ Process.setsid
168
+ exit if fork
169
+ Dir.chdir "/"
170
+ STDIN.reopen "/dev/null"
171
+ STDOUT.reopen "/dev/null", "a"
172
+ STDERR.reopen "/dev/null", "a"
173
+ else
174
+ Process.daemon
175
+ end
176
+ end
177
+
178
+ def write_pid_file
179
+ @log.debug("writing pid file #{@options[:server][:pid_file]}")
180
+ File.open(@options[:server][:pid_file], 'w') do |f|
181
+ f.write(Process.pid)
182
+ end
183
+ end
184
+
185
+ def execute
186
+ begin
187
+ ::EM.run do
188
+ ::EM.start_server @options[:server][:listen], @options[:server][:port], Server
189
+ end
190
+ rescue ArgumentException => e
191
+ @log.fatal(e.message)
192
+ rescue Interrupt => e
193
+ @log.info("exiting...")
194
+ rescue Exception => e
195
+ @log.fatal(e.message + e.backtrace.join("\n"))
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,181 @@
1
+ #
2
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of mysql_health.
6
+ #
7
+ # mysql_health is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # mysql_health is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'dbi'
21
+ require 'mysql'
22
+ require 'json'
23
+ require 'rufus-scheduler'
24
+
25
+ module MysqlHealth
26
+ class Health
27
+ @master_status = nil
28
+ @slave_status = nil
29
+ @scheduler = nil
30
+ @options = nil
31
+
32
+ def initialize(options = {})
33
+ @options = options
34
+
35
+ @mutex = Mutex.new
36
+ @scheduler = Rufus::Scheduler.start_new
37
+ def @scheduler.handle_exception(job, e)
38
+ MysqlHealth.log.error "job #{job.job_id} caught #{e.class} exception '#{e}' #{e.backtrace.join("\n")}"
39
+ end
40
+
41
+ if options[:master]
42
+ master_status = {}
43
+ master_status[:status] = 503
44
+ master_status[:content] = "Health of master not yet determined\n"
45
+ self.master_status=(master_status)
46
+ @scheduler.every options[:interval], :allow_overlapping => options[:allow_overlapping], :first_in => options[:delay] do
47
+ check_master
48
+ end
49
+ else
50
+ master_status = {}
51
+ master_status[:status] = '501 Not Enabled'
52
+ master_status[:content] = "Health of master not enabled\n"
53
+ self.master_status=(master_status)
54
+ end
55
+
56
+ if options[:slave]
57
+ slave_status = {}
58
+ slave_status[:status] = '501 Not Enabled'
59
+ slave_status[:content] = "Health of slave not yet determined\n"
60
+ self.slave_status=(slave_status)
61
+ @scheduler.every options[:interval], :allow_overlapping => options[:allow_overlapping], :first_in => options[:delay] do
62
+ check_slave
63
+ end
64
+ else
65
+ slave_status = {}
66
+ slave_status[:status] = '501 Not Enabled'
67
+ slave_status[:content] = "Health of slave not enabled\n"
68
+ self.slave_status=(slave_status)
69
+ end
70
+ end
71
+
72
+ def master_status=(response)
73
+ @mutex.synchronize do
74
+ MysqlHealth.log.info("master status: #{response[:status]}")
75
+ @master_status = response
76
+ end
77
+ end
78
+
79
+ def master_status
80
+ master_status = nil
81
+ @mutex.synchronize do
82
+ master_status = @master_status
83
+ end
84
+ return master_status
85
+ end
86
+
87
+ def slave_status=(response)
88
+ @mutex.synchronize do
89
+ MysqlHealth.log.info("slave status: #{response[:status]}")
90
+ @slave_status = response
91
+ end
92
+ end
93
+
94
+ def slave_status
95
+ slave_status = nil
96
+ @mutex.synchronize do
97
+ slave_status = @slave_status
98
+ end
99
+ return slave_status
100
+ end
101
+
102
+ def read_only?(dbh)
103
+ variables = dbh.select_all("SHOW VARIABLES WHERE Variable_name = 'read_only' AND Value = 'ON'")
104
+ return (variables.length == 1)
105
+ end
106
+
107
+ def check_master
108
+ MysqlHealth.log.debug("check_master")
109
+
110
+ # connect to the MySQL server
111
+ dbh = DBI.connect(@options[:dsn], @options[:username], @options[:password])
112
+
113
+ response = {}
114
+ response[:content_type] = 'text/plain'
115
+
116
+ status = {}
117
+ dbh.select_all('SHOW STATUS') do |row|
118
+ status[row[0].downcase.to_sym] = row[1]
119
+ end
120
+ mysqladmin_status = "Uptime: %s Threads: %s Questions: %s Slow queries: %s Opens: %s Flush tables: %s Open tables: %s Queries per second avg: %.3f\n" %
121
+ [ status[:uptime], status[:threads_running], status[:questions], status[:slow_queries], status[:opened_tables], status[:flush_commands], status[:open_tables], status[:queries].to_i/status[:uptime].to_i]
122
+ if status.length > 0
123
+ if read_only?(dbh)
124
+ response[:status] = '503 Service Read Only'
125
+ response[:content] = mysqladmin_status
126
+ else
127
+ response[:status] = '200 OK'
128
+ response[:content] = mysqladmin_status
129
+ end
130
+ else
131
+ response[:status] = '503 Service Unavailable'
132
+ response[:content] = mysqladmin_status
133
+ end
134
+ self.master_status=(response)
135
+ end
136
+
137
+ def check_slave
138
+ MysqlHealth.log.debug("check_slave")
139
+
140
+ # connect to the MySQL server
141
+ dbh = DBI.connect(@options[:dsn], @options[:username], @options[:password])
142
+
143
+ response = {}
144
+ response[:content_type] = 'text/plain'
145
+
146
+ show_slave_status = []
147
+ status = {}
148
+ dbh.select_all('SHOW SLAVE STATUS') do |row|
149
+ status[row[0].downcase.to_sym] = row[1]
150
+ show_slave_status << "#{row[0]}: #{row[1]}"
151
+ end
152
+
153
+ if status.length > 0
154
+ seconds_behind_master = status[:seconds_behind_master]
155
+
156
+ # We return a "203 Non-Authoritative Information" when replication is shot. We don't want to reduce site performance, but still want to track that something is awry.
157
+ if seconds_behind_master.eql?('NULL')
158
+ response[:status] = '203 Slave Stopped'
159
+ response[:content] = status.to_json
160
+ response[:content_type] = 'application/json'
161
+ elsif seconds_behind_master.to_i > 60*30
162
+ response[:status] = '203 Slave Behind'
163
+ response[:content] = status.to_json
164
+ response[:content_type] = 'application/json'
165
+ elsif read_only?(dbh)
166
+ response[:status] = '200 OK ' + seconds_behind_master + ' Seconds Behind Master'
167
+ response[:content] = status.to_json
168
+ response[:content_type] = 'application/json'
169
+ else
170
+ response[:status] = '503 Service Unavailable'
171
+ response[:content] = status.to_json
172
+ response[:content_type] = 'application/json'
173
+ end
174
+ else
175
+ response[:status] = '503 Slave Not Configured'
176
+ response[:content] = show_slave_status.join("\n")
177
+ end
178
+ self.slave_status=(response)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,76 @@
1
+ #
2
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of mysql_health.
6
+ #
7
+ # mysql_health is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # mysql_health is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'json'
21
+ require 'eventmachine'
22
+ require 'eventmachine_httpserver'
23
+ require 'evma_httpserver/response'
24
+ require 'forwardable'
25
+
26
+ module MysqlHealth
27
+ class Server < ::EM::Connection
28
+ include ::EM::HttpServer
29
+
30
+ def post_init
31
+ super
32
+ no_environment_strings
33
+ end
34
+
35
+ def http_response(data)
36
+ MysqlHealth.log.debug("http_response")
37
+ response = EventMachine::DelegatedHttpResponse.new(self)
38
+ if data.instance_of?(OpenStruct)
39
+ puts data.table.inspect
40
+ puts data.inspect
41
+ end
42
+
43
+ if data.nil?
44
+ response.status = '500 Server Error'
45
+ response.content = "Empty call to http_response\n"
46
+ else
47
+ data.each_pair do |k,v|
48
+ MysqlHealth.log.debug("#{k}=#{v}")
49
+ if k == :content_type
50
+ response.send(k, v)
51
+ else
52
+ response.send("#{k}=".to_sym, v)
53
+ end
54
+ end
55
+ end
56
+ response
57
+ end
58
+
59
+ def process_http_request
60
+ response = nil
61
+ begin
62
+ case @http_path_info
63
+ when '/master_status'
64
+ response = http_response(MysqlHealth.health.master_status)
65
+ when '/slave_status'
66
+ response = http_response(MysqlHealth.health.slave_status)
67
+ else
68
+ response = http_response({:status => '501 Not Implemented'})
69
+ end
70
+ rescue Exception => e
71
+ response = http_response({:status => '500 Server Error', :content => e.message + "\n" + e.backtrace.join("\n")})
72
+ end
73
+ response.send_response
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of mysql_health.
6
+ #
7
+ # mysql_health is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # mysql_health is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with mysql_health. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ module MysqlHealth
21
+ VERSION = "0.5.0"
22
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/mysql_health/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Erik Osterman"]
6
+ gem.email = ["e@osterman.com"]
7
+ gem.summary = %q{A service for monitoring MySQL and exposing its health through an HTTP interface.}
8
+ gem.description = %q{A service for monitoring MySQL and exposing its health through an HTTP interface for use with TCP load balancers (like haproxy) that support out-of-band health checks using HTTP.}
9
+ gem.homepage = "https://github.com/osterman/mysql_health"
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "mysql_health"
14
+ gem.license = 'GPLv3'
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MysqlHealth::VERSION
17
+ gem.add_runtime_dependency('rufus-scheduler', '>= 2.0.17')
18
+ gem.add_runtime_dependency('eventmachine', '>= 1.0.0.beta.4')
19
+ gem.add_runtime_dependency('eventmachine_httpserver', '>= 0.2.1')
20
+ gem.add_runtime_dependency('json', '>= 1.5.3')
21
+ gem.add_runtime_dependency('dbi', '>= 0.4.5')
22
+ gem.add_runtime_dependency('mysql', '>= 2.8.1')
23
+ end
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ #
3
+ # mysql_health - a service for monitoring MySQL and exposing its health through an HTTP interface.
4
+ #
5
+ # chkconfig: 345 99 99
6
+ # description: mysql_health
7
+ # processname: mysql_health
8
+
9
+
10
+ # Source function library.
11
+ . /etc/rc.d/init.d/functions
12
+
13
+ MYSQL_HEALTH_NICE="-n 19"
14
+ MYSQL_HEALTH_BIN="/usr/bin/mysql_health"
15
+ MYSQL_HEALTH_LOG="/var/log/mysql_health.log"
16
+ MYSQL_HEALTH_PID_FILE="/var/run/mysql_health.pid"
17
+ MYSQL_HEALTH_HOME="/tmp"
18
+ MYSQL_HEALTH_ARGS=""
19
+
20
+ [ -f /etc/sysconfig/mysql_health ] && . /etc/sysconfig/mysql_health
21
+
22
+ [ -x /usr/bin/mysql_health ] || exit 0
23
+
24
+
25
+ start_mysql_health()
26
+ {
27
+ cd $MYSQL_HEALTH_HOME
28
+ echo -n $"Starting mysql_health"
29
+ nice $MYSQL_HEALTH_NICE $MYSQL_HEALTH_BIN --server:pid-file="$MYSQL_HEALTH_PID_FILE" --log:file "$MYSQL_HEALTH_LOG" --server:daemonize $MYSQL_HEALTH_ARGS
30
+ result=$?
31
+ if [ "$result" -eq 0 ]; then
32
+ sleep 0.5
33
+ status -p "$MYSQL_HEALTH_PID_FILE" >/dev/null
34
+ result=$?
35
+ if [ "$result" -eq 0 ]; then
36
+ success
37
+ else
38
+ failure
39
+ fi
40
+ else
41
+ failure
42
+ fi
43
+ echo
44
+ return $result
45
+ }
46
+
47
+ stop_mysql_health()
48
+ {
49
+ echo -n $"Stopping mysql_health"
50
+
51
+ if [ -f "$MYSQL_HEALTH_PID_FILE" ]; then
52
+ kill $(cat "$MYSQL_HEALTH_PID_FILE") 2>/dev/null
53
+ result=$?
54
+ else
55
+ result=1
56
+ fi
57
+
58
+ if [ "$result" -eq 0 ]; then
59
+ result=1
60
+ for i in {1..60}; do
61
+ if [ -f "$MYSQL_HEALTH_PID_FILE" ]; then
62
+ echo -n "."
63
+ sleep 1
64
+ else
65
+ result=0
66
+ break
67
+ fi
68
+ done
69
+ fi
70
+
71
+ if [ "$result" -eq 0 ];then
72
+ success
73
+ else
74
+ failure
75
+ fi
76
+ echo
77
+ return $result
78
+ }
79
+
80
+
81
+ status_mysql_health() {
82
+ echo -n "mysql_health"
83
+ if [ -f "$MYSQL_HEALTH_PID_FILE" ]; then
84
+ status -p "$MYSQL_HEALTH_PID_FILE"
85
+ else
86
+ echo " is not running"
87
+ fi
88
+ }
89
+
90
+ status=0
91
+ # See how we were called.
92
+ case "$1" in
93
+ start)
94
+ start_mysql_health
95
+ status=$?
96
+ ;;
97
+ stop)
98
+ stop_mysql_health
99
+ status=$?
100
+ ;;
101
+ restart|reload)
102
+ stop_mysql_health
103
+ sleep 1
104
+ start_mysql_health
105
+ status=$?
106
+ ;;
107
+ status)
108
+ status_mysql_health
109
+ status=$?
110
+ ;;
111
+ *)
112
+ echo $"Usage: $0 {start|stop|restart|reload}"
113
+ status=1
114
+ esac
115
+
116
+ exit $status
117
+ exit 0
@@ -0,0 +1,73 @@
1
+ %define ruby_sitelib %(ruby -rrbconfig -e "puts Config::CONFIG['sitelibdir']")
2
+ %define gemdir %(ruby -rubygems -e 'puts Gem::dir' 2>/dev/null)
3
+ %define gemname mysql_health
4
+ %define gemversion 0.3.0
5
+ %define geminstdir %{gemdir}/gems/%{gemname}-%{gemversion}
6
+ %define gemfile %{gemname}-%{gemversion}.gem
7
+ %define gemsummary %(ruby -rrubygems -e 'puts YAML.load(`gem specification %{gemfile}`).summary')
8
+ %define gemdesc %(ruby -rrubygems -e 'puts YAML.load(`gem specification %{gemfile}`).description')
9
+ %define gemhomepage %(ruby -rrubygems -e 'puts YAML.load(`gem specification %{gemfile}`).homepage')
10
+ %define gemlicense %(ruby -rrubygems -e 'puts YAML.load(`gem specification %{gemfile}`).license || "Unknown"')
11
+ %define gemdeps %(ruby -rrubygems -e 'puts YAML.load(`gem specification %{gemfile}`.chomp).dependencies.map { |d| "rubygem(%s) %s" % [d.name, d.requirement] }.sort.join(", ")')
12
+ %define gemrelease %(date +"%%Y%%m%%d%%H%%M%%S")
13
+
14
+ Summary: %{gemsummary}
15
+ # The version is repeated in the name so as to allow multiple versions of the gem to be installed on the system.
16
+ Name: rubygem-%{gemname}-%{gemversion}
17
+ Version: %{gemversion}
18
+ Release: %{gemrelease}%{?dist}
19
+ Group: Development/Languages
20
+ License: %{gemlicense}
21
+ URL: %{gemhomepage}
22
+ Source0: http://rubygems.org/gems/%{gemname}-%{version}.gem
23
+ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
24
+ Requires: rubygems
25
+
26
+ Requires: %{gemdeps}
27
+
28
+ BuildRequires: rubygems
29
+ BuildRequires: rubygem(bundler)
30
+ BuildArch: noarch
31
+ Provides: rubygem(%{gemname}) = %{version}
32
+
33
+ %description
34
+ %{gemdesc}
35
+
36
+ %prep
37
+
38
+ %build
39
+
40
+ %install
41
+ rm -rf %{buildroot}
42
+ install --directory 0755 %{buildroot}%{gemdir}
43
+ gem install --local --install-dir %{buildroot}%{gemdir} \
44
+ --force --rdoc %{SOURCE0}
45
+ install --directory 0755 %{buildroot}/%{_bindir}
46
+ mv %{buildroot}%{gemdir}/bin/%{gemname} %{buildroot}/%{_bindir}
47
+ find %{buildroot}%{geminstdir}/bin -type f | xargs chmod a+x
48
+
49
+ install --directory --mode 0755 %{buildroot}%{_sysconfdir}/%{gemname}
50
+ install --directory --mode 0755 %{buildroot}%{_initrddir}
51
+ install --mode 755 %{buildroot}%{geminstdir}/redhat/%{gemname}.initrc %{buildroot}%{_initrddir}/%{gemname}
52
+
53
+ install --directory --mode 0755 %{buildroot}%{_sysconfdir}/sysconfig
54
+ cat<<__EOF__>%{buildroot}/%{_sysconfdir}/sysconfig/%{gemname}
55
+ __EOF__
56
+
57
+ %clean
58
+ rm -rf %{buildroot}
59
+
60
+ %files
61
+ %defattr(-, root, root, -)
62
+ %{_bindir}/%{gemname}
63
+ %{gemdir}/gems/%{gemname}-%{version}/
64
+ %doc %{gemdir}/doc/%{gemname}-%{version}
65
+ %{gemdir}/cache/%{gemname}-%{version}.gem
66
+ %{gemdir}/specifications/%{gemname}-%{version}.gemspec
67
+ %{_initrddir}/%{gemname}
68
+ %{_sysconfdir}/%{gemname}/
69
+ %{_sysconfdir}/sysconfig/%{gemname}
70
+
71
+ %changelog
72
+ * Sun Jul 29 2012 Erik Osterman <e@osterman.com>
73
+ - Initial package
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mysql_health
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 0
9
+ version: 0.5.0
10
+ platform: ruby
11
+ authors:
12
+ - Erik Osterman
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-07-29 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rufus-scheduler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 0
30
+ - 17
31
+ version: 2.0.17
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: eventmachine
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 0
44
+ - 0
45
+ - beta
46
+ - 4
47
+ version: 1.0.0.beta.4
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: eventmachine_httpserver
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ - 2
60
+ - 1
61
+ version: 0.2.1
62
+ type: :runtime
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: json
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ segments:
72
+ - 1
73
+ - 5
74
+ - 3
75
+ version: 1.5.3
76
+ type: :runtime
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: dbi
80
+ prerelease: false
81
+ requirement: &id005 !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ - 4
88
+ - 5
89
+ version: 0.4.5
90
+ type: :runtime
91
+ version_requirements: *id005
92
+ - !ruby/object:Gem::Dependency
93
+ name: mysql
94
+ prerelease: false
95
+ requirement: &id006 !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ segments:
100
+ - 2
101
+ - 8
102
+ - 1
103
+ version: 2.8.1
104
+ type: :runtime
105
+ version_requirements: *id006
106
+ description: A service for monitoring MySQL and exposing its health through an HTTP interface for use with TCP load balancers (like haproxy) that support out-of-band health checks using HTTP.
107
+ email:
108
+ - e@osterman.com
109
+ executables:
110
+ - mysql_health
111
+ extensions: []
112
+
113
+ extra_rdoc_files: []
114
+
115
+ files:
116
+ - .gitignore
117
+ - Gemfile
118
+ - LICENSE
119
+ - README.md
120
+ - Rakefile
121
+ - bin/mysql_health
122
+ - lib/mysql_health.rb
123
+ - lib/mysql_health/command_line.rb
124
+ - lib/mysql_health/health.rb
125
+ - lib/mysql_health/server.rb
126
+ - lib/mysql_health/version.rb
127
+ - mysql_health.gemspec
128
+ - redhat/mysql_health.initrc
129
+ - redhat/rubygem-mysql_health.spec
130
+ has_rdoc: true
131
+ homepage: https://github.com/osterman/mysql_health
132
+ licenses:
133
+ - GPLv3
134
+ post_install_message:
135
+ rdoc_options: []
136
+
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ segments:
144
+ - 0
145
+ version: "0"
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ segments:
151
+ - 0
152
+ version: "0"
153
+ requirements: []
154
+
155
+ rubyforge_project:
156
+ rubygems_version: 1.3.6
157
+ signing_key:
158
+ specification_version: 3
159
+ summary: A service for monitoring MySQL and exposing its health through an HTTP interface.
160
+ test_files: []
161
+