is_it_working 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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