health_check 3.0.0 → 3.1.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/Rakefile CHANGED
@@ -8,7 +8,7 @@ task :test do
8
8
  exec '/bin/bash', './test/test_with_railsapp'
9
9
  end
10
10
 
11
- task :default => :test
11
+ task default: :test
12
12
 
13
13
  begin
14
14
  gem 'rdoc'
data/Vagrantfile CHANGED
@@ -5,7 +5,7 @@ Vagrant.configure("2") do |config|
5
5
  # For a complete reference, please see the online documentation at
6
6
  # https://docs.vagrantup.com.
7
7
 
8
- config.vm.box = "ubuntu/xenial64"
8
+ config.vm.box = "ubuntu/focal64"
9
9
 
10
10
  # set auto_update to false, if you do NOT want to check the correct
11
11
  # additions version when booting this machine
@@ -17,4 +17,16 @@ Vagrant.configure("2") do |config|
17
17
  # provision with a shell script.
18
18
  config.vm.provision "shell", path: "./test/provision_vagrant"
19
19
 
20
+ config.vm.provider "virtualbox" do |v|
21
+ # travis allocates 7.5 GB, but this is sufficient
22
+ v.memory = 2048
23
+ v.cpus = 2
24
+ end
25
+
26
+ # if File.file?('.git') && IO.read('.git') =~ %r{\Agitdir: (.+)/.git/worktrees.*}
27
+ # # Handle git worktrees ...
28
+ # path = $1
29
+ # config.vm.synced_folder path, path
30
+ # end
31
+
20
32
  end
data/config/routes.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  unless HealthCheck::Engine.routes_explicitly_defined
2
- Rails.application.routes.draw do
2
+ ::Rails.application.routes.draw do
3
3
  add_health_check_routes()
4
4
  end
5
5
  end
data/health_check.gemspec CHANGED
@@ -9,14 +9,14 @@ Gem::Specification.new do |gem|
9
9
  gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version=
10
10
  gem.authors = ["Ian Heggie"]
11
11
  gem.email = ["ian@heggie.biz"]
12
- gem.summary = %q{Simple health check of Rails app for uptime monitoring with Pingdom, NewRelic, EngineYard or uptime.openacs.org etc.}
12
+ gem.summary = %q{Simple health check of Rails app for uptime monitoring with Pingdom, NewRelic, EngineYard etc.}
13
13
  gem.description = <<-EOF
14
- Simple health check of Rails app for uptime monitoring with Pingdom, NewRelic, EngineYard or uptime.openacs.org etc.
14
+ Simple health check of Rails app for uptime monitoring with Pingdom, NewRelic, EngineYard etc.
15
15
  EOF
16
16
  gem.homepage = "https://github.com/ianheggie/health_check"
17
+ gem.license = "MIT"
17
18
 
18
19
  gem.files = `git ls-files`.split($/)
19
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
20
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
21
  gem.extra_rdoc_files = [ "README.rdoc" ]
22
22
  gem.require_paths = ["lib"]
@@ -25,5 +25,5 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency(%q<smarter_bundler>, [">= 0.1.0"])
26
26
  gem.add_development_dependency(%q<rake>, [">= 0.8.3"])
27
27
  gem.add_development_dependency(%q<shoulda>, ["~> 2.11.0"])
28
- gem.add_development_dependency(%q<bundler>, ["~> 1.2"])
28
+ gem.add_development_dependency(%q<bundler>, [">= 1.2"])
29
29
  end
data/lib/health_check.rb CHANGED
@@ -3,14 +3,22 @@
3
3
 
4
4
  module HealthCheck
5
5
 
6
- class Engine < Rails::Engine
7
- cattr_accessor :routes_explicitly_defined
6
+ class Engine < ::Rails::Engine
7
+ cattr_accessor :routes_explicitly_defined
8
8
  end
9
9
 
10
+ # Log level
11
+ mattr_accessor :log_level
12
+ self.log_level = 'info'
13
+
10
14
  # Text output upon success
11
15
  mattr_accessor :success
12
16
  self.success = "success"
13
17
 
18
+ # Text output upon failure
19
+ mattr_accessor :failure
20
+ self.failure = "health_check failed"
21
+
14
22
  # Timeout in seconds used when checking smtp server
15
23
  mattr_accessor :smtp_timeout
16
24
  self.smtp_timeout = 30.0
@@ -27,6 +35,10 @@ module HealthCheck
27
35
  mattr_accessor :http_status_for_ip_whitelist_error
28
36
  self.http_status_for_ip_whitelist_error = 403
29
37
 
38
+ # check remote_ip rather than ip for ip whitelist
39
+ mattr_accessor :accept_proxied_requests
40
+ self.accept_proxied_requests = false
41
+
30
42
  # ips allowed to perform requests
31
43
  mattr_accessor :origin_ip_whitelist
32
44
  self.origin_ip_whitelist = []
@@ -40,6 +52,10 @@ module HealthCheck
40
52
  mattr_accessor :buckets
41
53
  self.buckets = {}
42
54
 
55
+ # rabbitmq
56
+ mattr_accessor :rabbitmq_config
57
+ self.rabbitmq_config = {}
58
+
43
59
  # health check uri path
44
60
  mattr_accessor :uri
45
61
  self.uri = 'health_check'
@@ -54,7 +70,7 @@ module HealthCheck
54
70
  mattr_accessor :full_checks
55
71
  mattr_accessor :standard_checks
56
72
  self.custom_checks = { }
57
- self.full_checks = ['database', 'migrations', 'custom', 'email', 'cache', 'redis-if-present', 'sidekiq-redis-if-present', 'resque-redis-if-present', 's3-if-present']
73
+ self.full_checks = ['database', 'migrations', 'custom', 'email', 'cache', 'redis-if-present', 'sidekiq-redis-if-present', 'resque-redis-if-present', 's3-if-present', 'elasticsearch-if-present']
58
74
  self.standard_checks = [ 'database', 'migrations', 'custom', 'emailconf' ]
59
75
 
60
76
  # Middleware based checks
@@ -63,15 +79,37 @@ module HealthCheck
63
79
 
64
80
  mattr_accessor :installed_as_middleware
65
81
 
66
- # Allow non-standard redis url
82
+ # Allow non-standard redis url and password
67
83
  mattr_accessor :redis_url
68
- self.redis_url = nil
84
+ self.redis_url = ENV['REDIS_URL']
85
+
86
+ mattr_accessor :redis_password
87
+ self.redis_password = 'some-password'
88
+
89
+ # Include the error in the response body.
90
+ # You should only do this where your /health_check endpoint is NOT open to the public internet
91
+ mattr_accessor :include_error_in_response_body
92
+ self.include_error_in_response_body = false
93
+
94
+ # used for on_failure and on_success
95
+ mattr_accessor :success_callbacks
96
+ mattr_accessor :failure_callbacks
69
97
 
70
98
  def self.add_custom_check(name = 'custom', &block)
71
99
  custom_checks[name] ||= [ ]
72
100
  custom_checks[name] << block
73
101
  end
74
102
 
103
+ def self.on_success(&block)
104
+ success_callbacks ||= [ ]
105
+ success_callbacks << block
106
+ end
107
+
108
+ def self.on_failure(&block)
109
+ failure_callbacks ||= [ ]
110
+ failure_callbacks << block
111
+ end
112
+
75
113
  def self.setup
76
114
  yield self
77
115
  end
@@ -83,10 +121,12 @@ require 'health_check/base_health_check'
83
121
  require 'health_check/resque_health_check'
84
122
  require 'health_check/s3_health_check'
85
123
  require 'health_check/redis_health_check'
124
+ require 'health_check/elasticsearch_health_check'
86
125
  require 'health_check/sidekiq_health_check'
87
126
  require 'health_check/utils'
88
127
  require 'health_check/health_check_controller'
89
128
  require 'health_check/health_check_routes'
90
129
  require 'health_check/middleware_health_check'
130
+ require 'health_check/rabbitmq_health_check'
91
131
 
92
132
  # vi: sw=2 sm ai:
@@ -0,0 +1,15 @@
1
+ module HealthCheck
2
+ class ElasticsearchHealthCheck
3
+ extend BaseHealthCheck
4
+
5
+ def self.check
6
+ unless defined?(::Elasticsearch)
7
+ raise "Wrong configuration. Missing 'elasticsearch' gem"
8
+ end
9
+ res = ::Elasticsearch::Client.new.ping
10
+ res == true ? '' : "Elasticsearch returned #{res.inspect} instead of true"
11
+ rescue Exception => e
12
+ create_error 'elasticsearch', e.message
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,6 @@
1
- # Copyright (c) 2010-2013 Ian Heggie, released under the MIT license.
1
+ # Copyright (c) 2010-2021 Ian Heggie, released under the MIT license.
2
2
  # See MIT-LICENSE for details.
3
+ require "ipaddr"
3
4
 
4
5
  module HealthCheck
5
6
  class HealthCheckController < ActionController::Base
@@ -14,8 +15,8 @@ module HealthCheck
14
15
  if max_age > 1
15
16
  last_modified = Time.at((last_modified.to_f / max_age).floor * max_age).utc
16
17
  end
17
- public = (max_age > 1) && ! HealthCheck.basic_auth_username
18
- if stale?(:last_modified => last_modified, :public => public)
18
+ is_public = (max_age > 1) && ! HealthCheck.basic_auth_username
19
+ if stale?(last_modified: last_modified, public: is_public)
19
20
  checks = params[:checks] ? params[:checks].split('_') : ['standard']
20
21
  checks -= HealthCheck.middleware_checks if HealthCheck.installed_as_middleware
21
22
  begin
@@ -23,15 +24,25 @@ module HealthCheck
23
24
  rescue Exception => e
24
25
  errors = e.message.blank? ? e.class.to_s : e.message.to_s
25
26
  end
26
- response.headers['Cache-control'] = (public ? 'public' : 'private') + ', no-cache, must-revalidate' + (max_age > 0 ? ", max-age=#{max_age}" : '')
27
+ response.headers['Cache-Control'] = "must-revalidate, max-age=#{max_age}"
27
28
  if errors.blank?
28
- send_response nil, :ok, :ok
29
+ send_response true, nil, :ok, :ok
30
+ if HealthCheck.success_callbacks
31
+ HealthCheck.success_callbacks.each do |callback|
32
+ callback.call(checks)
33
+ end
34
+ end
29
35
  else
30
- msg = "health_check failed: #{errors}"
31
- send_response msg, HealthCheck.http_status_for_error_text, HealthCheck.http_status_for_error_object
36
+ msg = HealthCheck.include_error_in_response_body ? "#{HealthCheck.failure}: #{errors}" : nil
37
+ send_response false, msg, HealthCheck.http_status_for_error_text, HealthCheck.http_status_for_error_object
38
+
32
39
  # Log a single line as some uptime checkers only record that it failed, not the text returned
33
- if logger
34
- logger.info msg
40
+ msg = "#{HealthCheck.failure}: #{errors}"
41
+ logger.send(HealthCheck.log_level, msg) if logger && HealthCheck.log_level
42
+ if HealthCheck.failure_callbacks
43
+ HealthCheck.failure_callbacks.each do |callback|
44
+ callback.call(checks, msg)
45
+ end
35
46
  end
36
47
  end
37
48
  end
@@ -39,15 +50,14 @@ module HealthCheck
39
50
 
40
51
  protected
41
52
 
42
- def send_response(msg, text_status, obj_status)
43
- healthy = !msg
44
- msg ||= HealthCheck.success
45
- obj = { :healthy => healthy, :message => msg}
53
+ def send_response(healthy, msg, text_status, obj_status)
54
+ msg ||= healthy ? HealthCheck.success : HealthCheck.failure
55
+ obj = { healthy: healthy, message: msg}
46
56
  respond_to do |format|
47
- format.html { render :plain => msg, :status => text_status, :content_type => 'text/plain' }
48
- format.json { render :json => obj, :status => obj_status }
49
- format.xml { render :xml => obj, :status => obj_status }
50
- format.any { render :plain => msg, :status => text_status, :content_type => 'text/plain' }
57
+ format.html { render plain: msg, status: text_status, content_type: 'text/plain' }
58
+ format.json { render json: obj, status: obj_status }
59
+ format.xml { render xml: obj, status: obj_status }
60
+ format.any { render plain: msg, status: text_status, content_type: 'text/plain' }
51
61
  end
52
62
  end
53
63
 
@@ -59,11 +69,12 @@ module HealthCheck
59
69
  end
60
70
 
61
71
  def check_origin_ip
72
+ request_ipaddr = IPAddr.new(HealthCheck.accept_proxied_requests ? request.remote_ip : request.ip)
62
73
  unless HealthCheck.origin_ip_whitelist.blank? ||
63
- HealthCheck.origin_ip_whitelist.include?(request.ip)
64
- render :plain => 'Health check is not allowed for the requesting IP',
65
- :status => HealthCheck.http_status_for_ip_whitelist_error,
66
- :content_type => 'text/plain'
74
+ HealthCheck.origin_ip_whitelist.any? { |addr| IPAddr.new(addr).include? request_ipaddr }
75
+ render plain: 'Health check is not allowed for the requesting IP',
76
+ status: HealthCheck.http_status_for_ip_whitelist_error,
77
+ content_type: 'text/plain'
67
78
  end
68
79
  end
69
80
 
@@ -8,7 +8,7 @@ module ActionDispatch::Routing
8
8
 
9
9
  def add_health_check_routes(prefix = nil)
10
10
  HealthCheck.uri = prefix if prefix
11
- match "#{HealthCheck.uri}(/:checks)(.:format)", :to => 'health_check/health_check#index', via: [:get, :post], :defaults => { :format => 'txt' }
11
+ match "#{HealthCheck.uri}(/:checks)(.:format)", controller: 'health_check/health_check', action: :index, via: [:get, :post], defaults: { format: 'txt' }
12
12
  end
13
13
 
14
14
  end
@@ -1,3 +1,7 @@
1
+ # Copyright (c) 2010-2021 Ian Heggie, released under the MIT license.
2
+ # See MIT-LICENSE for details.
3
+ require 'ipaddr'
4
+
1
5
  module HealthCheck
2
6
  class MiddlewareHealthcheck
3
7
 
@@ -59,7 +63,8 @@ module HealthCheck
59
63
  def ip_blocked(env)
60
64
  return false if HealthCheck.origin_ip_whitelist.blank?
61
65
  req = Rack::Request.new(env)
62
- unless HealthCheck.origin_ip_whitelist.include?(req.ip)
66
+ request_ipaddr = IPAddr.new(req.ip)
67
+ unless HealthCheck.origin_ip_whitelist.any? { |addr| IPAddr.new(addr).include? request_ipaddr }
63
68
  [ HealthCheck.http_status_for_ip_whitelist_error,
64
69
  { 'Content-Type' => 'text/plain' },
65
70
  [ 'Health check is not allowed for the requesting IP' ]
@@ -0,0 +1,16 @@
1
+ module HealthCheck
2
+ class RabbitMQHealthCheck
3
+ extend BaseHealthCheck
4
+ def self.check
5
+ unless defined?(::Bunny)
6
+ raise "Wrong configuration. Missing 'bunny' gem"
7
+ end
8
+ connection = Bunny.new(HealthCheck.rabbitmq_config)
9
+ connection.start
10
+ connection.close
11
+ ''
12
+ rescue Exception => e
13
+ create_error 'rabbitmq', e.message
14
+ end
15
+ end
16
+ end
@@ -1,15 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HealthCheck
2
4
  class RedisHealthCheck
3
5
  extend BaseHealthCheck
4
6
 
5
- def self.check
6
- unless defined?(::Redis)
7
- raise "Wrong configuration. Missing 'redis' gem"
7
+ class << self
8
+ def check
9
+ raise "Wrong configuration. Missing 'redis' gem" unless defined?(::Redis)
10
+
11
+ client.ping == 'PONG' ? '' : "Redis.ping returned #{res.inspect} instead of PONG"
12
+ rescue Exception => err
13
+ create_error 'redis', err.message
14
+ ensure
15
+ client.close if client.connected?
16
+ end
17
+
18
+ def client
19
+ @client ||= Redis.new(
20
+ {
21
+ url: HealthCheck.redis_url,
22
+ password: HealthCheck.redis_password
23
+ }.reject { |k, v| v.nil? }
24
+ )
8
25
  end
9
- res = ::Redis.new(url: HealthCheck.redis_url).ping
10
- res == 'PONG' ? '' : "Redis.ping returned #{res.inspect} instead of PONG"
11
- rescue Exception => e
12
- create_error 'redis', e.message
13
26
  end
14
27
  end
15
28
  end
@@ -5,7 +5,7 @@ module HealthCheck
5
5
  class << self
6
6
  def check
7
7
  unless defined?(::Aws)
8
- raise "Wrong configuration. Missing 'aws-sdk' gem"
8
+ raise "Wrong configuration. Missing 'aws-sdk' or 'aws-sdk-s3' gem"
9
9
  end
10
10
  return create_error 's3', 'Could not connect to aws' if aws_s3_client.nil?
11
11
  HealthCheck.buckets.each do |bucket_name, permissions|
@@ -27,19 +27,14 @@ module HealthCheck
27
27
 
28
28
  private
29
29
 
30
+ # We already assume you are using Rails. Let's also assume you have an initializer
31
+ # created for your Aws config. We will set the region here so you can use an
32
+ # instance profile and simply set the region in your environment.
30
33
  def configure_client
31
- return unless defined?(Rails)
34
+ ::Aws.config[:s3] = { force_path_style: true }
35
+ ::Aws.config[:region] ||= ENV['AWS_REGION'] || ENV['DEFAULT_AWS_REGION']
32
36
 
33
- aws_configuration = {
34
- region: Rails.application.secrets.aws_default_region,
35
- credentials: ::Aws::Credentials.new(
36
- Rails.application.secrets.aws_access_key_id,
37
- Rails.application.secrets.aws_secret_access_key
38
- ),
39
- force_path_style: true
40
- }
41
-
42
- ::Aws::S3::Client.new aws_configuration
37
+ ::Aws::S3::Client.new
43
38
  end
44
39
 
45
40
  def aws_s3_client
@@ -52,13 +47,13 @@ module HealthCheck
52
47
 
53
48
  def W(bucket)
54
49
  aws_s3_client.put_object(bucket: bucket,
55
- key: "healthcheck_#{Rails.application.class.parent_name}",
50
+ key: "healthcheck_#{::Rails.application.class.parent_name}",
56
51
  body: Time.new.to_s)
57
52
  end
58
53
 
59
54
  def D(bucket)
60
55
  aws_s3_client.delete_object(bucket: bucket,
61
- key: "healthcheck_#{Rails.application.class.parent_name}")
56
+ key: "healthcheck_#{::Rails.application.class.parent_name}")
62
57
  end
63
58
  end
64
59
  end
@@ -6,13 +6,13 @@ module HealthCheck
6
6
 
7
7
  @@default_smtp_settings =
8
8
  {
9
- :address => "localhost",
10
- :port => 25,
11
- :domain => 'localhost.localdomain',
12
- :user_name => nil,
13
- :password => nil,
14
- :authentication => nil,
15
- :enable_starttls_auto => true,
9
+ address: "localhost",
10
+ port: 25,
11
+ domain: 'localhost.localdomain',
12
+ user_name: nil,
13
+ password: nil,
14
+ authentication: nil,
15
+ enable_starttls_auto: true
16
16
  }
17
17
 
18
18
  cattr_accessor :default_smtp_settings
@@ -55,6 +55,8 @@ module HealthCheck
55
55
  errors << HealthCheck::RedisHealthCheck.check if defined?(::Redis)
56
56
  when 's3-if-present'
57
57
  errors << HealthCheck::S3HealthCheck.check if defined?(::Aws)
58
+ when 'elasticsearch-if-present'
59
+ errors << HealthCheck::ElasticsearchHealthCheck.check if defined?(::Elasticsearch)
58
60
  when 'resque-redis'
59
61
  errors << HealthCheck::ResqueHealthCheck.check
60
62
  when 'sidekiq-redis'
@@ -63,6 +65,10 @@ module HealthCheck
63
65
  errors << HealthCheck::RedisHealthCheck.check
64
66
  when 's3'
65
67
  errors << HealthCheck::S3HealthCheck.check
68
+ when 'elasticsearch'
69
+ errors << HealthCheck::ElasticsearchHealthCheck.check
70
+ when 'rabbitmq'
71
+ errors << HealthCheck::RabbitMQHealthCheck.check
66
72
  when "standard"
67
73
  errors << HealthCheck::Utils.process_checks(HealthCheck.standard_checks, called_from_middleware)
68
74
  when "middleware"
@@ -84,15 +90,16 @@ module HealthCheck
84
90
  return "invalid argument to health_test."
85
91
  end
86
92
  end
93
+ errors << '. ' unless errors == '' || errors.end_with?('. ')
87
94
  end
88
- return errors
95
+ return errors.strip
89
96
  rescue => e
90
97
  return e.message
91
98
  end
92
99
 
93
100
  def self.db_migrate_path
94
101
  # Lazy initialisation so Rails.root will be defined
95
- @@db_migrate_path ||= File.join(Rails.root, 'db', 'migrate')
102
+ @@db_migrate_path ||= File.join(::Rails.root, 'db', 'migrate')
96
103
  end
97
104
 
98
105
  def self.db_migrate_path=(value)
@@ -135,36 +142,43 @@ module HealthCheck
135
142
  status = ''
136
143
  begin
137
144
  if @skip_external_checks
138
- status = '221'
145
+ status = '250'
139
146
  else
140
- Timeout::timeout(timeout) do |timeout_length|
141
- t = TCPSocket.new(settings[:address], settings[:port])
142
- begin
143
- status = t.gets
144
- while status != nil && status !~ /^2/
145
- status = t.gets
146
- end
147
- t.puts "HELO #{settings[:domain]}\r"
148
- while status != nil && status !~ /^250/
149
- status = t.gets
150
- end
151
- t.puts "QUIT\r"
152
- status = t.gets
153
- ensure
154
- t.close
155
- end
147
+ smtp = Net::SMTP.new(settings[:address], settings[:port])
148
+ smtp.enable_starttls if settings[:enable_starttls_auto]
149
+ smtp.open_timeout = timeout
150
+ smtp.read_timeout = timeout
151
+ smtp.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do
152
+ status = smtp.helo(settings[:domain]).status
156
153
  end
157
154
  end
158
- rescue Errno::EBADF => ex
159
- status = "Unable to connect to service"
160
155
  rescue Exception => ex
161
156
  status = ex.to_s
162
157
  end
163
- (status =~ /^221/) ? '' : "SMTP: #{status || 'unexpected EOF on socket'}. "
158
+ (status =~ /^250/) ? '' : "SMTP: #{status || 'unexpected error'}. "
164
159
  end
165
160
 
166
161
  def self.check_cache
167
- Rails.cache.write('__health_check_cache_test__', 'ok', :expires_in => 1.second) ? '' : 'Unable to write to cache. '
162
+ t = Time.now.to_i
163
+ value = "ok #{t}"
164
+ ret = ::Rails.cache.read('__health_check_cache_test__')
165
+ if ret.to_s =~ /^ok (\d+)$/
166
+ diff = ($1.to_i - t).abs
167
+ return('Cache expiry is broken. ') if diff > 30
168
+ elsif ret
169
+ return 'Cache is returning garbage. '
170
+ end
171
+ if ::Rails.cache.write('__health_check_cache_test__', value, expires_in: 2.seconds)
172
+ ret = ::Rails.cache.read('__health_check_cache_test__')
173
+ if ret =~ /^ok (\d+)$/
174
+ diff = ($1.to_i - t).abs
175
+ (diff < 2 ? '' : 'Out of date cache or time is skewed. ')
176
+ else
177
+ 'Unable to read from cache. '
178
+ end
179
+ else
180
+ 'Unable to write to cache. '
181
+ end
168
182
  end
169
183
 
170
184
  end