is_it_working 1.0.10

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.
@@ -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,152 @@
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
+ messages << "#{m.ok? ? 'OK: ' : 'FAIL:'} #{status.name} - #{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms)"
138
+ end
139
+ end
140
+
141
+ info = []
142
+ info << "Host: #{@hostname}" unless @hostname.size == 0
143
+ info << "PID: #{$$}"
144
+ info << "Timestamp: #{Time.now.iso8601}"
145
+ info << "Elapsed Time: #{(elapsed_time * 1000).round}ms"
146
+
147
+ code = (fail ? 200 : 500)
148
+
149
+ [code, headers, [info.join("\n"), "\n\n", messages.join("\n")]]
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,50 @@
1
+ module IsItWorking
2
+ # This class is used to pass the status of a monitoring check. Each status can have multiple
3
+ # messages added to it by calling the +ok+ or +fail+ methods. The status check will only be
4
+ # considered a success if all messages are ok.
5
+ class Status
6
+ # This class is used to contain individual status messages. Eache method can represent either
7
+ # and +ok+ message or a +fail+ message.
8
+ class Message
9
+ attr_reader :message
10
+
11
+ def initialize(message, ok)
12
+ @message = message
13
+ @ok = ok
14
+ end
15
+
16
+ def ok?
17
+ @ok
18
+ end
19
+ end
20
+
21
+ # The name of the status check for display purposes.
22
+ attr_reader :name
23
+
24
+ # The messages set on the status check.
25
+ attr_reader :messages
26
+
27
+ # The amount of time it takes to complete the status check.
28
+ attr_accessor :time
29
+
30
+ def initialize(name)
31
+ @name = name
32
+ @messages = []
33
+ end
34
+
35
+ # Add a message indicating that the check passed.
36
+ def ok(message)
37
+ @messages << Message.new(message, true)
38
+ end
39
+
40
+ # Add a message indicating that the check failed.
41
+ def fail(message)
42
+ @messages << Message.new(message, false)
43
+ end
44
+
45
+ # Returns +true+ only if all checks were OK.
46
+ def success?
47
+ @messages.all?{|m| m.ok?}
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe IsItWorking::ActionMailerCheck do
4
+
5
+ let(:status){ IsItWorking::Status.new(:ping) }
6
+
7
+ it "should succeed if the default mail host is accepting connections" do
8
+ ActionMailer::Base.smtp_settings[:address] = 'localhost'
9
+ ActionMailer::Base.smtp_settings[:port] = 25
10
+ TCPSocket.should_receive(:new).with('localhost', 25).and_return(mock(:socket, :close => true))
11
+ check = IsItWorking::ActionMailerCheck.new
12
+ check.call(status)
13
+ status.should be_success
14
+ status.messages.first.message.should == "ActionMailer::Base is accepting connections on port 25"
15
+ end
16
+
17
+ it "should succeed if the default mail host is not accepting connections" do
18
+ ActionMailer::Base.smtp_settings[:address] = 'localhost'
19
+ ActionMailer::Base.smtp_settings[:port] = 25
20
+ TCPSocket.should_receive(:new).with('localhost', 25).and_raise(Errno::ECONNREFUSED)
21
+ check = IsItWorking::ActionMailerCheck.new
22
+ check.call(status)
23
+ status.should_not be_success
24
+ status.messages.first.message.should == "ActionMailer::Base is not accepting connections on port 25"
25
+ end
26
+
27
+ it "should get the smtp configuration from a specified ActionMailer class" do
28
+ class IsItWorking::ActionMailerCheck::Tester < ActionMailer::Base
29
+ end
30
+
31
+ IsItWorking::ActionMailerCheck::Tester.smtp_settings[:address] = 'mail.example.com'
32
+ IsItWorking::ActionMailerCheck::Tester.smtp_settings[:port] = 'smtp'
33
+ TCPSocket.should_receive(:new).with('mail.example.com', 'smtp').and_return(mock(:socket, :close => true))
34
+ check = IsItWorking::ActionMailerCheck.new(:class => IsItWorking::ActionMailerCheck::Tester)
35
+ check.call(status)
36
+ status.should be_success
37
+ status.messages.first.message.should == "IsItWorking::ActionMailerCheck::Tester is accepting connections on port \"smtp\""
38
+ end
39
+
40
+ it "should allow aliasing the ActionMailer host alias" do
41
+ ActionMailer::Base.smtp_settings[:address] = 'localhost'
42
+ ActionMailer::Base.smtp_settings[:port] = 25
43
+ TCPSocket.should_receive(:new).with('localhost', 25).and_return(mock(:socket, :close => true))
44
+ check = IsItWorking::ActionMailerCheck.new(:alias => "smtp host")
45
+ check.call(status)
46
+ status.should be_success
47
+ status.messages.first.message.should == "smtp host is accepting connections on port 25"
48
+ end
49
+
50
+
51
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe IsItWorking::ActiveRecordCheck do
4
+
5
+ let(:status){ IsItWorking::Status.new(:active_record) }
6
+
7
+ class IsItWorking::TestActiveRecord < ActiveRecord::Base
8
+ end
9
+
10
+ it "should succeed if the ActiveRecord connection is active" do
11
+ connection = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(mock(:connection))
12
+ connection.reconnect!
13
+ ActiveRecord::Base.stub!(:connection).and_return(connection)
14
+ check = IsItWorking::ActiveRecordCheck.new
15
+ check.call(status)
16
+ status.should be_success
17
+ status.messages.first.message.should == "ActiveRecord::Base.connection is active"
18
+ end
19
+
20
+ it "should allow specifying the class to check the connection for" do
21
+ connection = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(mock(:connection))
22
+ connection.reconnect!
23
+ IsItWorking::TestActiveRecord.stub!(:connection).and_return(connection)
24
+ check = IsItWorking::ActiveRecordCheck.new(:class => IsItWorking::TestActiveRecord)
25
+ check.call(status)
26
+ status.should be_success
27
+ status.messages.first.message.should == "IsItWorking::TestActiveRecord.connection is active"
28
+ end
29
+
30
+ it "should succeed if the ActiveRecord connection can be reconnected" do
31
+ connection = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(mock(:connection))
32
+ connection.disconnect!
33
+ ActiveRecord::Base.stub!(:connection).and_return(connection)
34
+ check = IsItWorking::ActiveRecordCheck.new
35
+ check.call(status)
36
+ status.should be_success
37
+ status.messages.first.message.should == "ActiveRecord::Base.connection is active"
38
+ end
39
+
40
+ it "should fail if the ActiveRecord connection is not active" do
41
+ connection = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(mock(:connection))
42
+ connection.disconnect!
43
+ connection.stub!(:verify!)
44
+ ActiveRecord::Base.stub!(:connection).and_return(connection)
45
+ check = IsItWorking::ActiveRecordCheck.new
46
+ check.call(status)
47
+ status.should_not be_success
48
+ status.messages.first.message.should == "ActiveRecord::Base.connection is not active"
49
+ end
50
+
51
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ describe IsItWorking::DalliCheck do
4
+
5
+ let(:status){ IsItWorking::Status.new(:memcache) }
6
+ let(:memcache){ Dalli::Client.new(['cache-1.example.com', 'cache-2.example.com']) }
7
+ let(:servers){ memcache.send(:ring).servers }
8
+
9
+ it "should succeed if all servers are responding" do
10
+ check = IsItWorking::DalliCheck.new(:cache => memcache)
11
+ servers.first.should_receive(:alive?).and_return(true)
12
+ servers.last.should_receive(:alive?).and_return(true)
13
+ check.call(status)
14
+ status.should be_success
15
+ status.messages.first.message.should == "cache-1.example.com:11211 is available"
16
+ status.messages.last.message.should == "cache-2.example.com:11211 is available"
17
+ end
18
+
19
+ it "should fail if any server is not responding" do
20
+ check = IsItWorking::DalliCheck.new(:cache => memcache)
21
+ servers.first.should_receive(:alive?).and_return(true)
22
+ servers.last.should_receive(:alive?).and_return(false)
23
+ check.call(status)
24
+ status.should_not be_success
25
+ status.messages.first.message.should == "cache-1.example.com:11211 is available"
26
+ status.messages.last.message.should == "cache-2.example.com:11211 is not available"
27
+ end
28
+
29
+ it "should be able to get the MemCache object from an ActiveSupport::Cache" do
30
+ require 'active_support/cache'
31
+ require 'active_support/cache/dalli_store'
32
+ ActiveSupport::Cache::DalliStore.should_receive(:new).with('cache-1.example.com', 'cache-2.example.com').and_return(memcache)
33
+ rails_cache = ActiveSupport::Cache::DalliStore.new('cache-1.example.com', 'cache-2.example.com')
34
+ check = IsItWorking::DalliCheck.new(:cache => rails_cache)
35
+ servers.first.should_receive(:alive?).and_return(true)
36
+ servers.last.should_receive(:alive?).and_return(true)
37
+ check.call(status)
38
+ status.should be_success
39
+ status.messages.first.message.should == "cache-1.example.com:11211 is available"
40
+ status.messages.last.message.should == "cache-2.example.com:11211 is available"
41
+ end
42
+
43
+ it "should be able to alias the memcache host names in the output" do
44
+ check = IsItWorking::DalliCheck.new(:cache => memcache, :alias => "memcache")
45
+ servers.first.should_receive(:alive?).and_return(true)
46
+ servers.last.should_receive(:alive?).and_return(true)
47
+ check.call(status)
48
+ status.should be_success
49
+ status.messages.first.message.should == "memcache 1 is available"
50
+ status.messages.last.message.should == "memcache 2 is available"
51
+ end
52
+
53
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ describe IsItWorking::DirectoryCheck do
4
+
5
+ let(:status){ IsItWorking::Status.new(:directory) }
6
+ let(:directory_path){ File.expand_path(".") }
7
+
8
+ it "should fail if a directory can't be found" do
9
+ check = IsItWorking::DirectoryCheck.new(:path => File.expand_path("../no_such_thing", __FILE__))
10
+ check.call(status)
11
+ status.should_not be_success
12
+ status.messages.first.message.should include("does not exist")
13
+ end
14
+
15
+ it "should fail if a path isn't a directory" do
16
+ check = IsItWorking::DirectoryCheck.new(:path => __FILE__)
17
+ check.call(status)
18
+ status.should_not be_success
19
+ status.messages.first.message.should include("is not a directory")
20
+ end
21
+
22
+ it "should succeed if a directory exists" do
23
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path)
24
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => false, :writable? => false))
25
+ check.call(status)
26
+ status.should be_success
27
+ status.messages.first.message.should include("exists")
28
+ end
29
+
30
+ it "should fail if a directory is not readable" do
31
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path, :permission => :read)
32
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => false, :writable? => true))
33
+ check.call(status)
34
+ status.should_not be_success
35
+ status.messages.first.message.should include("is not readable")
36
+ end
37
+
38
+ it "should fail if a directory is not writable" do
39
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path, :permission => :write)
40
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => true, :writable? => false))
41
+ check.call(status)
42
+ status.should_not be_success
43
+ status.messages.first.message.should include("is not writable")
44
+ end
45
+
46
+ it "should succeed if a directory exists and is readable" do
47
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path, :permission => :read)
48
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => true, :writable? => false))
49
+ check.call(status)
50
+ status.should be_success
51
+ status.messages.first.message.should include("exists with")
52
+ end
53
+
54
+ it "should succeed if a directory exists and is writable" do
55
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path, :permission => :write)
56
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => false, :writable? => true))
57
+ check.call(status)
58
+ status.should be_success
59
+ status.messages.first.message.should include("exists with")
60
+ end
61
+
62
+ it "should succeed if a directory exists and is readable and writable" do
63
+ check = IsItWorking::DirectoryCheck.new(:path => directory_path, :permission => [:read, :write])
64
+ File.should_receive(:stat).with(directory_path).and_return(mock(:stat, :directory? => true, :readable? => true, :writable? => true))
65
+ check.call(status)
66
+ status.should be_success
67
+ status.messages.first.message.should include("exists with")
68
+ end
69
+
70
+ end