is_it_working-cbeer 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,75 @@
1
+ = Is It Working
2
+
3
+ This gem provides a mechanism for setting up a Rack handler that tests the status of various components of an application and reports the output. It is designed to be modular and give a comprehensive view of the application status with a consistent URL (/is_it_working by default).
4
+
5
+ This handler can be used by monitoring to determine if an application is working or not, but it does not replace system level monitoring of low level resources. Rather it adds another level which tells if the application can actually use those resources.
6
+
7
+ == Use It As Documentation
8
+
9
+ A feature of this gem is that it gives you a consistent place to document the external dependencies of you application as code. The handler checking the status of you application should have a check for every line drawn from it to another box on a system architecture diagram.
10
+
11
+ == Example
12
+
13
+ Suppose you have a Rails application that uses the following services:
14
+
15
+ * ActiveRecord uses PostgreSQL database
16
+ * Caching is done using Rails.cache with a cluster of memcached instances
17
+ * Web service API hosted at https://api.example.com
18
+ * NFS shared directory symlinked to from system/data in the Rails root directory
19
+ * SMTP server at mail.example.com
20
+ * A black box service encapsulated in AwesomeService
21
+
22
+ A monitoring handler for this set up could be set up in <tt>config/initializers/is_it_working.rb</tt> like this:
23
+
24
+ Rails.configuration.middleware.use(IsItWorking::Handler) do |h|
25
+ # Check the ActiveRecord database connection without spawning a new thread
26
+ h.check :active_record, :async => false
27
+
28
+ # Check the memcache servers used by Rails.cache if using the MemCacheStore implementation
29
+ h.check :memcache, :cache => Rails.cache if Rails.cache.is_a?(ActiveSupport::Cache::MemCacheStore)
30
+
31
+ # Check that the web service is working by hitting a known URL with Basic authentication
32
+ h.check :url, :get => "http://api.example.com/version", :username => "appname", :password => "abc123"
33
+
34
+ # Check that the NFS mount directory is available with read/write permissions
35
+ h.check :directory, :path => Rails.root + "system/data", :permission => [:read, :write]
36
+
37
+ # Check the mail server configured for ActionMailer
38
+ h.check :action_mailer if ActionMailer::Base.delivery_method == :smtp
39
+
40
+ # Ping another mail server
41
+ h.check :ping, :host => "mail.example.com", :port => "smtp"
42
+
43
+ # Check that AwesomeService is working using the service's own logic
44
+ h.check :awesome_service do |status|
45
+ if AwesomeService.active?
46
+ status.ok("service active")
47
+ else
48
+ status.fail("service down")
49
+ end
50
+ end
51
+ end
52
+
53
+ == Output
54
+
55
+ The response from the handler will be a plain text description of the checks that were run and the results of those checks. If all the checks passed, the response code will be 200. If any checks fail, the response code will be 500. The response will look something like this:
56
+
57
+ Host: example.com
58
+ PID: 696
59
+ Timestamp: 2011-01-13T16:55:13-06:00
60
+ Elapsed Time: 84ms
61
+
62
+ OK: active_record - ActiveRecord::Base.connection is active (2.516ms)
63
+ OK: memcache - cache1.example.com:11211 is available (0.022ms)
64
+ OK: memcache - cache2.example.com:11211 is available (0.022ms)
65
+ OK: url - GET http://www.example.com/ responded with response '200 OK' (81.775ms)
66
+ OK: directory - /app/myapp/system/data exists with read/write permission (0.044ms)
67
+ OK: ping - mail.example.com is accepting connections on port "smtp" (61.854ms)
68
+
69
+ == Security
70
+
71
+ Keep in mind that the output from the status check will be available on a publicly accessible URL. This can pose a security risk if some servers are not on a private network behind a firewall. If necessary, you can obscure the host names in the predefined checks by providing an <tt>:alias</tt> option that will be output instead of the actual host name or IP address. Also, you can manually specify the hostname that the handler reports for the application with the Handler#hostname= method.
72
+
73
+ == Thread Safety
74
+
75
+ By default status checks each happen in their own thread. If you write your own status check, you must make sure it is thread safe. If you need to synchronize the check logic, you can use the +synchronize+ method on the handler object to do so. Alternatively, you can pass <tt>:async => false</tt> to any +check+ specification. This will cause the check to be executed in the main request thread.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'rake'
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ desc 'Default: run unit tests'
12
+ task :default => :test
13
+
14
+ require 'rspec'
15
+ require 'rspec/core/rake_task'
16
+ desc 'Run the unit tests'
17
+ RSpec::Core::RakeTask.new(:test)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.11
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "is_it_working-cbeer"
5
+ gem.version = File.read(File.expand_path('../VERSION', __FILE__))
6
+ gem.summary = %Q{Rack handler for monitoring several parts of a web application.}
7
+ gem.description = %Q{Rack handler for monitoring several parts of a web application so one request can determine which system or dependencies are down.}
8
+ gem.authors = ["Brian Durand", "Chris Beer"]
9
+ gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com", "chris@cbeer.info"]
10
+ gem.files = `git ls-files`.split("\n")
11
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ gem.has_rdoc = true
14
+ gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
15
+ gem.extra_rdoc_files = ["README.rdoc"]
16
+
17
+ gem.add_development_dependency('rspec', '>= 2.0')
18
+ gem.add_development_dependency('webmock', '>= 1.6.0')
19
+ gem.add_development_dependency('memcache-client')
20
+ gem.add_development_dependency('dalli')
21
+ gem.add_development_dependency('rails')
22
+ end
23
+
@@ -0,0 +1,25 @@
1
+ require 'action_mailer'
2
+
3
+ module IsItWorking
4
+ # Check if the mail server configured for ActionMailer is responding.
5
+ #
6
+ # The ActionMailer class that yields the configuration can be specified with the <tt>:class</tt>
7
+ # option. By default this will be ActionMailer::Base. You can also set a <tt>:timeout</tt> option
8
+ # for how long to wait for a response and an <tt>:alias</tt> option which will be the name reported
9
+ # back by the check (defaults to the ActionMailer class).
10
+ #
11
+ # === Example
12
+ #
13
+ # IsItWorking::Handler.new do |h|
14
+ # h.check :action_mailer, :class => UserMailer
15
+ # end
16
+ class ActionMailerCheck < PingCheck
17
+ def initialize(options={})
18
+ options = options.dup
19
+ klass = options.delete(:class) || ActionMailer::Base
20
+ options.merge!(:host => klass.smtp_settings[:address], :port => klass.smtp_settings[:port] || 'smtp')
21
+ options[:alias] ||= klass.name
22
+ super(options)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require 'active_record'
2
+
3
+ module IsItWorking
4
+ # Check if the database connection used by an ActiveRecord class is up.
5
+ #
6
+ # The ActiveRecord class that yields the connection can be specified with the <tt>:class</tt>
7
+ # option. By default this will be ActiveRecord::Base.
8
+ #
9
+ # === Example
10
+ #
11
+ # IsItWorking::Handler.new do |h|
12
+ # h.check :active_record, :class => User
13
+ # end
14
+ class ActiveRecordCheck
15
+ def initialize(options={})
16
+ @class = options[:class] || ActiveRecord::Base
17
+ end
18
+
19
+ def call(status)
20
+ @class.connection.verify!
21
+ if @class.connection.active?
22
+ status.ok("#{@class}.connection is active")
23
+ else
24
+ status.fail("#{@class}.connection is not active")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ require 'dalli'
2
+
3
+ module IsItWorking
4
+ class DalliCheck
5
+ # Check if all the memcached servers in a cluster are responding.
6
+ # The memcache cluster to check is specified with the <tt>:cache</tt> options. The
7
+ # value can be either a Dalli::Client object (from the dalli gem) or an
8
+ # ActiveSupport::Cache::DalliStore (i.e. Rails.cache).
9
+ #
10
+ # If making the IP addresses of the memcache servers known to the world could
11
+ # pose a security risk because they are not on a private network behind a firewall,
12
+ # you can provide the <tt>:alias</tt> option to change the host names that are reported.
13
+ #
14
+ # === Example
15
+ #
16
+ # IsItWorking::Handler.new do |h|
17
+ # h.check :dalli, :cache => Rails.cache, :alias => "memcache server"
18
+ # end
19
+ def initialize(options={})
20
+ memcache = options[:cache]
21
+ raise ArgumentError.new(":cache not specified") unless memcache
22
+ unless memcache.is_a?(Dalli::Client)
23
+ if defined?(ActiveSupport::Cache::DalliStore) && memcache.is_a?(ActiveSupport::Cache::DalliStore)
24
+ # Big hack to get the MemCache object from Rails.cache
25
+ @memcache = memcache.instance_variable_get(:@data)
26
+ else
27
+ raise ArgumentError.new("#{memcache} is not a Dalli::Client")
28
+ end
29
+ else
30
+ @memcache = memcache
31
+ end
32
+ @alias = options[:alias]
33
+ end
34
+
35
+ def call(status)
36
+ servers = @memcache.send(:ring).servers
37
+ servers.each_with_index do |server, i|
38
+ public_host_name = @alias ? "#{@alias} #{i + 1}" : "#{server.hostname}:#{server.port}"
39
+
40
+ if server.alive?
41
+ status.ok("#{public_host_name} is available")
42
+ else
43
+ status.fail("#{public_host_name} is not available")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ module IsItWorking
2
+ class DirectoryCheck
3
+ # Check if a file system directory exists and has the correct access. This
4
+ # can be very useful to check if the application relies on a shared file sytem
5
+ # being mounted. The <tt>:path</tt> options must be supplied to the initializer. You
6
+ # may also supply an <tt>:permission</tt> option with the values <tt>:read</tt>, <tt>:write</tt>, or
7
+ # <tt>[:read, :write]</tt> to check the permission on the directory as well.
8
+ #
9
+ # === Example
10
+ #
11
+ # IsItWorking::Handler.new do |h|
12
+ # h.check :directory, :path => "/var/shared/myapp", :permission => [:read, :write]
13
+ # end
14
+ def initialize (options={})
15
+ raise ArgumentError.new(":path not specified") unless options[:path]
16
+ @path = File.expand_path(options[:path])
17
+ @permission = options[:permission]
18
+ @permission = [@permission] if @permission && !@permission.is_a?(Array)
19
+ end
20
+
21
+ def call(status)
22
+ stat = File.stat(@path) if File.exist?(@path)
23
+ if stat
24
+ if stat.directory?
25
+ if @permission
26
+ if @permission.include?(:read) && !stat.readable?
27
+ status.fail("#{@path} is not readable by #{ENV['USER']}")
28
+ elsif @permission.include?(:write) && !stat.writable?
29
+ status.fail("#{@path} is not writable by #{ENV['USER']}")
30
+ else
31
+ status.ok("#{@path} exists with #{@permission.collect{|a| a.to_s}.join('/')} permission")
32
+ end
33
+ else
34
+ status.ok("#{@path} exists")
35
+ end
36
+ else
37
+ status.fail("#{@path} is not a directory")
38
+ end
39
+ else
40
+ status.fail("#{@path} does not exist")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ require 'memcache'
2
+
3
+ module IsItWorking
4
+ class MemcacheCheck
5
+ # Check if all the memcached servers in a cluster are responding.
6
+ # The memcache cluster to check is specified with the <tt>:cache</tt> options. The
7
+ # value can be either a MemCache object (from the memcache-client gem) or an
8
+ # ActiveSupport::Cache::MemCacheStore (i.e. Rails.cache).
9
+ #
10
+ # If making the IP addresses of the memcache servers known to the world could
11
+ # pose a security risk because they are not on a private network behind a firewall,
12
+ # you can provide the <tt>:alias</tt> option to change the host names that are reported.
13
+ #
14
+ # === Example
15
+ #
16
+ # IsItWorking::Handler.new do |h|
17
+ # h.check :memcache, :cache => Rails.cache, :alias => "memcache server"
18
+ # end
19
+ def initialize(options={})
20
+ memcache = options[:cache]
21
+ raise ArgumentError.new(":cache not specified") unless memcache
22
+ unless memcache.is_a?(MemCache)
23
+ if defined?(ActiveSupport::Cache::MemCacheStore) && memcache.is_a?(ActiveSupport::Cache::MemCacheStore)
24
+ # Big hack to get the MemCache object from Rails.cache
25
+ @memcache = memcache.instance_variable_get(:@data)
26
+ else
27
+ raise ArgumentError.new("#{memcache} is not a MemCache")
28
+ end
29
+ else
30
+ @memcache = memcache
31
+ end
32
+ @alias = options[:alias]
33
+ end
34
+
35
+ def call(status)
36
+ @memcache.servers.each_with_index do |server, i|
37
+ public_host_name = @alias ? "#{@alias} #{i + 1}" : "#{server.host}:#{server.port}"
38
+ if server.alive?
39
+ status.ok("#{public_host_name} is available")
40
+ else
41
+ status.fail("#{public_host_name} is not available")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ module IsItWorking
5
+ class PingCheck
6
+ # Check if a host is reachable and accepting connections on a specified port.
7
+ #
8
+ # The host and port to ping are specified with the <tt>:host</tt> and <tt>:port</tt> options. The port
9
+ # can be either a port number or port name for a well known port (i.e. "smtp" and 25 are
10
+ # equivalent). The default timeout to wait for a response is 2 seconds. This can be
11
+ # changed with the <tt>:timeout</tt> option.
12
+ #
13
+ # By default, the host name will be included in the output. If this could pose a security
14
+ # risk by making the existence of the host known to the world, you can supply the <tt>:alias</tt>
15
+ # option which will be used for output purposes. In general, you should supply this option
16
+ # unless the host is on a private network behind a firewall.
17
+ #
18
+ # === Example
19
+ #
20
+ # IsItWorking::Handler.new do |h|
21
+ # h.check :ping, :host => "example.com", :port => "ftp", :timeout => 4
22
+ # end
23
+ def initialize(options={})
24
+ @host = options[:host]
25
+ raise ArgumentError.new(":host not specified") unless @host
26
+ @port = options[:port]
27
+ raise ArgumentError.new(":port not specified") unless @port
28
+ @timeout = options[:timeout] || 2
29
+ @alias = options[:alias] || @host
30
+ end
31
+
32
+ def call(status)
33
+ begin
34
+ ping(@host, @port)
35
+ status.ok("#{@alias} is accepting connections on port #{@port.inspect}")
36
+ rescue Errno::ECONNREFUSED
37
+ status.fail("#{@alias} is not accepting connections on port #{@port.inspect}")
38
+ rescue SocketError => e
39
+ status.fail("connection to #{@alias} on port #{@port.inspect} failed with '#{e.message}'")
40
+ rescue Timeout::Error
41
+ status.fail("#{@alias} did not respond on port #{@port.inspect} within #{@timeout} seconds")
42
+ end
43
+ end
44
+
45
+ def ping(host, port)
46
+ timeout(@timeout) do
47
+ s = TCPSocket.new(host, port)
48
+ s.close
49
+ end
50
+ true
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module IsItWorking
5
+ # Check if getting a URL returns a successful response. Only responses in the range 2xx or 304
6
+ # are considered successful. Redirects will not be followed.
7
+ #
8
+ # Available options are:
9
+ #
10
+ # * <tt>:get</tt> - The URL to get.
11
+ # * <tt>:headers</tt> - Hash of headers to send with the request
12
+ # * <tt>:proxy</tt> - Hash of proxy server information. The hash must contain a <tt>:host</tt> key and may contain <tt>:port</tt>, <tt>:username</tt>, and <tt>:password</tt>
13
+ # * <tt>:username</tt> - Username to use for Basic Authentication
14
+ # * <tt>:password</tt> - Password to use for Basic Authentication
15
+ # * <tt>:open_timeout</tt> - Time in seconds to wait for opening the connection (defaults to 5 seconds)
16
+ # * <tt>:read_timeout</tt> - Time in seconds to wait for data from the connection (defaults to 10 seconds)
17
+ # * <tt>:alias</tt> - Alias used for reporting in case making the URL known to the world could provide a security risk.
18
+ #
19
+ # === Example
20
+ #
21
+ # IsItWorking::Handler.new do |h|
22
+ # h.check :url, :get => "http://services.example.com/api", :headers => {"Accept" => "text/xml"}
23
+ # end
24
+ class UrlCheck
25
+ def initialize(options={})
26
+ raise ArgumentError.new(":get must provide the URL to check") unless options[:get]
27
+ @uri = URI.parse(options[:get])
28
+ @headers = options[:headers] || {}
29
+ @proxy = options[:proxy]
30
+ @username = options[:username]
31
+ @password = options[:password]
32
+ @open_timeout = options[:open_timeout] || 5
33
+ @read_timeout = options[:read_timeout] || 10
34
+ @alias = options[:alias] || options[:get]
35
+ end
36
+
37
+ def call(status)
38
+ t = Time.now
39
+ response = perform_http_request
40
+ if response.is_a?(Net::HTTPSuccess)
41
+ status.ok("GET #{@alias} responded with response '#{response.code} #{response.message}'")
42
+ else
43
+ status.fail("GET #{@alias} failed with response '#{response.code} #{response.message}'")
44
+ end
45
+ rescue Timeout::Error
46
+ status.fail("GET #{@alias} timed out after #{Time.now - t} seconds")
47
+ end
48
+
49
+ private
50
+ # Create an HTTP object with the options set.
51
+ def instantiate_http #:nodoc:
52
+ http_class = nil
53
+
54
+ if @proxy && @proxy[:host]
55
+ http_class = Net::HTTP::Proxy(@proxy[:host], @proxy[:port], @proxy[:username], @proxy[:password])
56
+ else
57
+ http_class = Net::HTTP
58
+ end
59
+
60
+ http = http_class.new(@uri.host, @uri.port)
61
+ if @uri.scheme == 'https'
62
+ http.use_ssl = true
63
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
64
+ end
65
+ http.open_timeout = @open_timeout
66
+ http.read_timeout = @read_timeout
67
+
68
+ return http
69
+ end
70
+
71
+ # Perform an HTTP request and return the response
72
+ def perform_http_request #:nodoc:
73
+ request = Net::HTTP::Get.new(@uri.request_uri, @headers)
74
+ request.basic_auth(@username, @password) if @username || @password
75
+ http = instantiate_http
76
+ http.start do
77
+ http.request(request)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ module IsItWorking
2
+ # Wrapper around a status check.
3
+ class Filter
4
+ class AsyncRunner < Thread
5
+ attr_accessor :filter_status
6
+ end
7
+
8
+ class SyncRunner
9
+ attr_accessor :filter_status
10
+
11
+ def initialize
12
+ yield
13
+ end
14
+
15
+ def join
16
+ end
17
+ end
18
+
19
+ attr_reader :name, :async
20
+
21
+ # Create a new filter to run a status check. The name is used for display purposes.
22
+ def initialize(name, check, async = true)
23
+ @name = name
24
+ @check = check
25
+ @async = async
26
+ end
27
+
28
+ # Run a status the status check. This method keeps track of the time it took to run
29
+ # the check and will trap any unexpected exceptions and report them as failures.
30
+ def run
31
+ status = Status.new(name)
32
+ runner = (async ? AsyncRunner : SyncRunner).new do
33
+ t = Time.now
34
+ begin
35
+ @check.call(status)
36
+ rescue Exception => e
37
+ status.fail("#{name} error: #{e.inspect}")
38
+ end
39
+ status.time = Time.now - t
40
+ end
41
+ runner.filter_status = status
42
+ runner
43
+ end
44
+
45
+ class << self
46
+ # Run a list of filters and return their status objects
47
+ def run_filters (filters)
48
+ runners = filters.collect{|f| f.run}
49
+ statuses = runners.collect{|runner| runner.filter_status}
50
+ runners.each{|runner| runner.join}
51
+ statuses
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,153 @@
1
+ module IsItWorking
2
+ # Rack handler that will run a set of status checks on the application and report the
3
+ # results. The results are formatted in plain text. If any of the checks fails, the
4
+ # response code will be 500 Server Error.
5
+ #
6
+ # The checks to perform are defined in the initialization block. Each check needs a name
7
+ # and can either be a predefined check, block, or an object that responds to the +call+
8
+ # method. When a check is called, its +call+ method will be called with a Status object.
9
+ #
10
+ # === Example
11
+ #
12
+ # IsItWorkingHandler.new do |h|
13
+ # # Predefined check to determine if a directory is accessible
14
+ # h.check :directory, "/var/myapp", :read, :write
15
+ #
16
+ # # Custom check using a block
17
+ # h.check :solr do
18
+ # SolrServer.available? ? ok("solr is up") : fail("solr is down")
19
+ # end
20
+ # end
21
+ class Handler
22
+ PATH_INFO = "PATH_INFO".freeze
23
+
24
+ # Create a new handler. This method can take a block which will yield itself so it can
25
+ # be configured.
26
+ #
27
+ # The handler can be set up in one of two ways. If no arguments are supplied, it will
28
+ # return a regular Rack handler that can be used with a rackup +run+ method or in a
29
+ # Rails 3+ routes.rb file. Otherwise, an application stack can be supplied in the first
30
+ # argument and a routing path in the second (defaults to <tt>/is_it_working</tt>) so
31
+ # it can be used with the rackup +use+ method or in Rails.middleware.
32
+ def initialize(app=nil, route_path="/is_it_working", &block)
33
+ @app = app
34
+ @route_path = route_path
35
+ @hostname = `hostname`.chomp
36
+ @filters = []
37
+ @mutex = Mutex.new
38
+ yield self if block_given?
39
+ end
40
+
41
+ def call(env)
42
+ if @app.nil? || env[PATH_INFO] == @route_path
43
+ statuses = []
44
+ t = Time.now
45
+ statuses = Filter.run_filters(@filters)
46
+ render(statuses, Time.now - t)
47
+ else
48
+ @app.call(env)
49
+ end
50
+ end
51
+
52
+ # Set the hostname reported the the application is running on. By default this is set
53
+ # the system hostname. You should override it if the value reported as the hostname by
54
+ # the system is not useful or if exposing it publicly would create a security risk.
55
+ def hostname=(val)
56
+ @hostname = val
57
+ end
58
+
59
+ # Add a status check to the handler.
60
+ #
61
+ # If a block is given, it will be used as the status check and will be yielded to
62
+ # with a Status object.
63
+ #
64
+ # If the name matches one of the pre-defined status check classes, a new instance will
65
+ # be created using the rest of the arguments as the arguments to the initializer. The
66
+ # pre-defined classes are:
67
+ #
68
+ # * <tt>:action_mailer</tt> - Check if the send mail configuration used by ActionMailer is available
69
+ # * <tt>:active_record</tt> - Check if the database connection for an ActiveRecord class is up
70
+ # * <tt>:dalli</tt> - DalliCheck checks if all the servers in a MemCache cluster are available using dalli
71
+ # * <tt>:directory</tt> - DirectoryCheck checks for the accessibilty of a file system directory
72
+ # * <tt>:memcache</tt> - MemcacheCheck checks if all the servers in a MemCache cluster are available using memcache-client
73
+ # * <tt>:ping</tt> - Check if a host is reachable and accepting connections on a port
74
+ # * <tt>:url</tt> - Check if a getting a URL returns a success response
75
+ def check (name, *options_or_check, &block)
76
+ raise ArgumentError("Too many arguments to #{self.class.name}#check") if options_or_check.size > 2
77
+ check = nil
78
+ options = {:async => true}
79
+
80
+ unless options_or_check.empty?
81
+ if options_or_check[0].is_a?(Hash)
82
+ options = options.merge(options_or_check[0])
83
+ else
84
+ check = options_or_check[0]
85
+ end
86
+ if options_or_check[1].is_a?(Hash)
87
+ options = options.merge(options_or_check[1])
88
+ end
89
+ end
90
+
91
+ unless check
92
+ if block
93
+ check = block
94
+ else
95
+ check = lookup_check(name, options)
96
+ end
97
+ end
98
+
99
+ @filters << Filter.new(name, check, options[:async])
100
+ end
101
+
102
+ # Helper method to synchronize a block of code so it can be thread safe.
103
+ # This method uses a Mutex and is not re-entrant. The synchronization will
104
+ # be only on calls to this handler.
105
+ def synchronize
106
+ @mutex.synchronize do
107
+ yield
108
+ end
109
+ end
110
+
111
+ protected
112
+ # Lookup a status check filter from the name and arguments
113
+ def lookup_check(name, options) #:nodoc:
114
+ check_class_name = "#{name.to_s.gsub(/(^|_)([a-z])/){|m| m.sub('_', '').upcase}}Check"
115
+ check = nil
116
+ if IsItWorking.const_defined?(check_class_name)
117
+ check_class = IsItWorking.const_get(check_class_name)
118
+ check = check_class.new(options)
119
+ else
120
+ raise ArgumentError.new("Check not defined #{check_class_name}")
121
+ end
122
+ check
123
+ end
124
+
125
+ # Output the plain text response from calling all the filters.
126
+ def render(statuses, elapsed_time) #:nodoc:
127
+ fail = statuses.all?{|s| s.success?}
128
+ headers = {
129
+ "Content-Type" => "text/plain; charset=utf8",
130
+ "Cache-Control" => "no-cache",
131
+ "Date" => Time.now.httpdate,
132
+ }
133
+
134
+ messages = []
135
+ statuses.each do |status|
136
+ status.messages.each do |m|
137
+
138
+ messages << "#{sprintf '%-5s', m.state.to_s.upcase}: #{status.name} - #{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms)"
139
+ end
140
+ end
141
+
142
+ info = []
143
+ info << "Host: #{@hostname}" unless @hostname.size == 0
144
+ info << "PID: #{$$}"
145
+ info << "Timestamp: #{Time.now.iso8601}"
146
+ info << "Elapsed Time: #{(elapsed_time * 1000).round}ms"
147
+
148
+ code = (fail ? 200 : 500)
149
+
150
+ [code, headers, [info.join("\n"), "\n\n", messages.join("\n")]]
151
+ end
152
+ end
153
+ end