module IsItWorking
# Rack handler that will run a set of status checks on the application and report the
# results. The results are formatted in plain text. If any of the checks fails, the
# response code will be 500 Server Error.
#
# The checks to perform are defined in the initialization block. Each check needs a name
# and can either be a predefined check, block, or an object that responds to the +call+
# method. When a check is called, its +call+ method will be called with a Status object.
#
# === Example
#
# IsItWorkingHandler.new do |h|
# # Predefined check to determine if a directory is accessible
# h.check :directory, "/var/myapp", :read, :write
#
# # Custom check using a block
# h.check :solr do
# SolrServer.available? ? ok("solr is up") : fail("solr is down")
# end
# end
class Handler
PATH_INFO = "PATH_INFO".freeze
# Create a new handler. This method can take a block which will yield itself so it can
# be configured.
#
# The handler can be set up in one of two ways. If no arguments are supplied, it will
# return a regular Rack handler that can be used with a rackup +run+ method or in a
# Rails 3+ routes.rb file. Otherwise, an application stack can be supplied in the first
# argument and a routing path in the second (defaults to /is_it_working) so
# it can be used with the rackup +use+ method or in Rails.middleware.
def initialize(app=nil, route_path="/is_it_working", &block)
@app = app
@route_path = route_path
@hostname = `hostname`.chomp
@filters = []
@mutex = Mutex.new
yield self if block_given?
end
def call(env)
if @app.nil? || env[PATH_INFO] == @route_path
statuses = []
t = Time.now
statuses = Filter.run_filters(@filters)
render(statuses, Time.now - t)
else
@app.call(env)
end
end
# Set the hostname reported the the application is running on. By default this is set
# the system hostname. You should override it if the value reported as the hostname by
# the system is not useful or if exposing it publicly would create a security risk.
def hostname=(val)
@hostname = val
end
# Add a status check to the handler.
#
# If a block is given, it will be used as the status check and will be yielded to
# with a Status object.
#
# If the name matches one of the pre-defined status check classes, a new instance will
# be created using the rest of the arguments as the arguments to the initializer. The
# pre-defined classes are:
#
# * :action_mailer - Check if the send mail configuration used by ActionMailer is available
# * :active_record - Check if the database connection for an ActiveRecord class is up
# * :dalli - DalliCheck checks if all the servers in a MemCache cluster are available using dalli
# * :directory - DirectoryCheck checks for the accessibilty of a file system directory
# * :memcache - MemcacheCheck checks if all the servers in a MemCache cluster are available using memcache-client
# * :ping - Check if a host is reachable and accepting connections on a port
# * :url - Check if a getting a URL returns a success response
def check (name, *options_or_check, &block)
raise ArgumentError("Too many arguments to #{self.class.name}#check") if options_or_check.size > 2
check = nil
options = {:async => true}
unless options_or_check.empty?
if options_or_check[0].is_a?(Hash)
options = options.merge(options_or_check[0])
else
check = options_or_check[0]
end
if options_or_check[1].is_a?(Hash)
options = options.merge(options_or_check[1])
end
end
unless check
if block
check = block
else
check = lookup_check(name, options)
end
end
@filters << Filter.new(name, check, options[:async])
end
# Helper method to synchronize a block of code so it can be thread safe.
# This method uses a Mutex and is not re-entrant. The synchronization will
# be only on calls to this handler.
def synchronize
@mutex.synchronize do
yield
end
end
protected
# Lookup a status check filter from the name and arguments
def lookup_check(name, options) #:nodoc:
check_class_name = "#{name.to_s.gsub(/(^|_)([a-z])/){|m| m.sub('_', '').upcase}}Check"
check = nil
if IsItWorking.const_defined?(check_class_name)
check_class = IsItWorking.const_get(check_class_name)
check = check_class.new(options)
else
raise ArgumentError.new("Check not defined #{check_class_name}")
end
check
end
# Output the plain text response from calling all the filters.
def render(statuses, elapsed_time) #:nodoc:
fail = statuses.all?{|s| s.success?}
headers = {
"Content-Type" => "text/plain; charset=utf8",
"Cache-Control" => "no-cache",
"Date" => Time.now.httpdate,
}
messages = []
statuses.each do |status|
status.messages.each do |m|
messages << "#{m.ok? ? 'OK: ' : 'FAIL:'} #{status.name} - #{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms)"
end
end
info = []
info << "Host: #{@hostname}" unless @hostname.size == 0
info << "PID: #{$$}"
info << "Timestamp: #{Time.now.iso8601}"
info << "Elapsed Time: #{(elapsed_time * 1000).round}ms"
code = (fail ? 200 : 500)
[code, headers, [info.join("\n"), "\n\n", messages.join("\n")]]
end
end
end