is_it_working 1.0.10

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,38 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ desc 'Default: run unit tests'
5
+ task :default => :test
6
+
7
+ begin
8
+ require 'rspec'
9
+ require 'rspec/core/rake_task'
10
+ desc 'Run the unit tests'
11
+ RSpec::Core::RakeTask.new(:test)
12
+ rescue LoadError
13
+ task :test do
14
+ raise "You must have rspec 2.0 installed to run the tests"
15
+ end
16
+ end
17
+
18
+ begin
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |gem|
21
+ gem.name = "is_it_working"
22
+ gem.summary = %Q{Rack handler for monitoring several parts of a web application.}
23
+ gem.description = %Q{Rack handler for monitoring several parts of a web application so one request can determine which system or dependencies are down.}
24
+ gem.authors = ["Brian Durand"]
25
+ gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
26
+ gem.files = FileList["lib/**/*", "spec/**/*", "bin/**/*", "example/**/*" "README.rdoc", "Rakefile", "License.txt"].to_a
27
+ gem.has_rdoc = true
28
+ gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
29
+ gem.extra_rdoc_files = ["README.rdoc"]
30
+ # Add dependencies with gem.add_dependency('gem_name')
31
+ gem.add_development_dependency('rspec', '>= 2.0')
32
+ gem.add_development_dependency('webmock', '>= 1.6.0')
33
+ gem.add_development_dependency('memcache-client')
34
+ gem.add_development_dependency('dalli')
35
+ end
36
+ Jeweler::RubygemsDotOrgTasks.new
37
+ rescue LoadError
38
+ end
@@ -0,0 +1,18 @@
1
+ require 'time'
2
+ require 'thread'
3
+
4
+ module IsItWorking
5
+ autoload :Check, File.expand_path("../is_it_working/check.rb", __FILE__)
6
+ autoload :Filter, File.expand_path("../is_it_working/filter.rb", __FILE__)
7
+ autoload :Handler, File.expand_path("../is_it_working/handler.rb", __FILE__)
8
+ autoload :Status, File.expand_path("../is_it_working/status.rb", __FILE__)
9
+
10
+ # Predefined checks
11
+ autoload :ActionMailerCheck, File.expand_path("../is_it_working/checks/action_mailer_check.rb", __FILE__)
12
+ autoload :ActiveRecordCheck, File.expand_path("../is_it_working/checks/active_record_check.rb", __FILE__)
13
+ autoload :DalliCheck, File.expand_path("../is_it_working/checks/dalli_check.rb", __FILE__)
14
+ autoload :DirectoryCheck, File.expand_path("../is_it_working/checks/directory_check.rb", __FILE__)
15
+ autoload :MemcacheCheck, File.expand_path("../is_it_working/checks/memcache_check.rb", __FILE__)
16
+ autoload :PingCheck, File.expand_path("../is_it_working/checks/ping_check.rb", __FILE__)
17
+ autoload :UrlCheck, File.expand_path("../is_it_working/checks/url_check.rb", __FILE__)
18
+ end
@@ -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