tribune-is_it_working 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,201 @@
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", app_name='', &block)
33
+ @app = app
34
+ @route_path = route_path
35
+ @hostname = `hostname`.chomp
36
+ @filters = []
37
+ @mutex = Mutex.new
38
+ @app_name = app_name # app name dss-recurly|ssor|dss-main etc..
39
+ yield self if block_given?
40
+ end
41
+
42
+ def call(env)
43
+ if @app.nil? || env[PATH_INFO] == @route_path
44
+ statuses = []
45
+ t = Time.now
46
+ statuses = Filter.run_filters(@filters)
47
+ if @route_path.eql?('/is_it_working')
48
+ render(statuses, Time.now - t)
49
+ else
50
+ # return JSON response instead of TEXT for non 'is_it_working' route URL
51
+ render_json(statuses, Time.now - t)
52
+ end
53
+ else
54
+ @app.call(env)
55
+ end
56
+ end
57
+
58
+ # Set the hostname reported the the application is running on. By default this is set
59
+ # the system hostname. You should override it if the value reported as the hostname by
60
+ # the system is not useful or if exposing it publicly would create a security risk.
61
+ def hostname=(val)
62
+ @hostname = val
63
+ end
64
+
65
+ # Add a status check to the handler.
66
+ #
67
+ # If a block is given, it will be used as the status check and will be yielded to
68
+ # with a Status object.
69
+ #
70
+ # If the name matches one of the pre-defined status check classes, a new instance will
71
+ # be created using the rest of the arguments as the arguments to the initializer. The
72
+ # pre-defined classes are:
73
+ #
74
+ # * <tt>:action_mailer</tt> - Check if the send mail configuration used by ActionMailer is available
75
+ # * <tt>:active_record</tt> - Check if the database connection for an ActiveRecord class is up
76
+ # * <tt>:dalli</tt> - DalliCheck checks if all the servers in a MemCache cluster are available using dalli
77
+ # * <tt>:directory</tt> - DirectoryCheck checks for the accessibilty of a file system directory
78
+ # * <tt>:memcache</tt> - MemcacheCheck checks if all the servers in a MemCache cluster are available using memcache-client
79
+ # * <tt>:ping</tt> - Check if a host is reachable and accepting connections on a port
80
+ # * <tt>:url</tt> - Check if a getting a URL returns a success response
81
+ def check (name, *options_or_check, &block)
82
+ raise ArgumentError("Too many arguments to #{self.class.name}#check") if options_or_check.size > 2
83
+ check = nil
84
+ options = {:async => true}
85
+
86
+ unless options_or_check.empty?
87
+ if options_or_check[0].is_a?(Hash)
88
+ options = options.merge(options_or_check[0])
89
+ else
90
+ check = options_or_check[0]
91
+ end
92
+ if options_or_check[1].is_a?(Hash)
93
+ options = options.merge(options_or_check[1])
94
+ end
95
+ end
96
+
97
+ unless check
98
+ if block
99
+ check = block
100
+ else
101
+ check = lookup_check(name, options)
102
+ end
103
+ end
104
+ # New Params:
105
+ # @param: component_name [String] optional
106
+ # => for which component checking health-check (ex: dss-url, zephr-url, active_record etc)
107
+ # @param: description [String] optional
108
+ # => Description for health-check for component
109
+ @filters << IsItWorking::Filter.new(name, options[:component_name],
110
+ options[:description], check, options[:async])
111
+ end
112
+
113
+ # Helper method to synchronize a block of code so it can be thread safe.
114
+ # This method uses a Mutex and is not re-entrant. The synchronization will
115
+ # be only on calls to this handler.
116
+ def synchronize
117
+ @mutex.synchronize do
118
+ yield
119
+ end
120
+ end
121
+
122
+ protected
123
+ # Lookup a status check filter from the name and arguments
124
+ def lookup_check(name, options) #:nodoc:
125
+ check_class_name = "#{name.to_s.gsub(/(^|_)([a-z])/){|m| m.sub('_', '').upcase}}Check"
126
+ check = nil
127
+ if IsItWorking.const_defined?(check_class_name)
128
+ check_class = IsItWorking.const_get(check_class_name)
129
+ check = check_class.new(options)
130
+ else
131
+ raise ArgumentError.new("Check not defined #{check_class_name}")
132
+ end
133
+ check
134
+ end
135
+
136
+ # Output the plain text response from calling all the filters.
137
+ def render(statuses, elapsed_time) #:nodoc:
138
+ fail = statuses.all?{|s| s.success?}
139
+ headers = {
140
+ "Content-Type" => "text/plain; charset=utf8",
141
+ "Cache-Control" => "no-cache",
142
+ "Date" => Time.now.httpdate,
143
+ }
144
+
145
+ messages = []
146
+ statuses.each do |status|
147
+ status.messages.each do |m|
148
+ messages << "#{m.ok? ? 'OK: ' : 'FAIL:'} #{status.name} - #{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms)"
149
+ end
150
+ end
151
+
152
+ info = []
153
+ info << "Host: #{@hostname}" unless @hostname.size == 0
154
+ info << "PID: #{$$}"
155
+ info << "Timestamp: #{Time.now.iso8601}"
156
+ info << "Elapsed Time: #{(elapsed_time * 1000).round}ms"
157
+
158
+ code = (fail ? 200 : 500)
159
+
160
+ [code, headers, [info.join("\n"), "\n\n", messages.join("\n")]]
161
+ end
162
+
163
+ # Seperate method for rendeing/returning JSON
164
+ def render_json(statuses, elapsed_time) #:nodoc:
165
+ fail = statuses.all?{|s| s.success?}
166
+ messages = []
167
+ components = []
168
+ total_fail_count = 0
169
+ statuses.each do |status|
170
+ status.messages.each do |m|
171
+ messages << "#{m.ok? ? 'OK: ' : 'FAIL:'} #{status.name} - #{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms)"
172
+ message = { name: status.name,
173
+ component_name: status.component_name,
174
+ info: "#{m.message} (#{status.time ? sprintf('%0.000f', status.time * 1000) : '?'}ms",
175
+ time: status.time,
176
+ success: m.ok? ? true : false,
177
+ description: status.description }
178
+ total_fail_count += 1 unless m.ok?
179
+ components << message
180
+ end
181
+ end
182
+
183
+ code = (fail ? 200 : 500)
184
+
185
+ final_json = {
186
+ "healthcheck": {
187
+ "status_code": code,
188
+ "pid": "#{$$}",
189
+ "host": @hostname,
190
+ "app_name": @app_name,
191
+ "time_stamp": Time.now.iso8601,
192
+ "elapsed_time": "#{(elapsed_time * 1000).round}ms",
193
+ "final_status": total_fail_count > 0 ? 'FAIL' : 'PASS', #fail ? 'FAIL' : 'PASS',
194
+ "total_fail_count": total_fail_count,
195
+ "components": components
196
+ }
197
+ }
198
+ final_json
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,55 @@
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
+ # name and description of Component
25
+ attr_reader :component_name, :description
26
+
27
+ # The messages set on the status check.
28
+ attr_reader :messages
29
+
30
+ # The amount of time it takes to complete the status check.
31
+ attr_accessor :time
32
+
33
+ def initialize(name, component_name, description)
34
+ @name = name
35
+ @component_name = component_name
36
+ @description = description
37
+ @messages = []
38
+ end
39
+
40
+ # Add a message indicating that the check passed.
41
+ def ok(message)
42
+ @messages << Message.new(message, true)
43
+ end
44
+
45
+ # Add a message indicating that the check failed.
46
+ def fail(message)
47
+ @messages << Message.new(message, false)
48
+ end
49
+
50
+ # Returns +true+ only if all checks were OK.
51
+ def success?
52
+ @messages.all?{|m| m.ok?}
53
+ end
54
+ end
55
+ 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,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
@@ -0,0 +1,46 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe IsItWorking::Filter do
4
+
5
+ it "should have a name" do
6
+ filter = IsItWorking::Filter.new(:test, lambda{})
7
+ filter.name.should == :test
8
+ end
9
+
10
+ it "should run a check and return a thread" do
11
+ check = lambda do |status|
12
+ status.ok("success")
13
+ end
14
+
15
+ filter = IsItWorking::Filter.new(:test, check)
16
+ runner = filter.run
17
+ status = runner.filter_status
18
+ runner.join
19
+ status.should be_success
20
+ status.messages.first.message.should == "success"
21
+ status.time.should_not be_nil
22
+ end
23
+
24
+ it "should run a check and recue an errors" do
25
+ check = lambda do |status|
26
+ raise "boom!"
27
+ end
28
+
29
+ filter = IsItWorking::Filter.new(:test, check)
30
+ runner = filter.run
31
+ status = runner.filter_status
32
+ runner.join
33
+ status.should_not be_success
34
+ status.messages.first.message.should include("boom")
35
+ status.time.should_not be_nil
36
+ end
37
+
38
+ it "should run multiple filters and return their statuses" do
39
+ filter_1 = IsItWorking::Filter.new(:test, lambda{|status| status.ok("OK")})
40
+ filter_2 = IsItWorking::Filter.new(:test, lambda{|status| status.fail("FAIL")})
41
+ statuses = IsItWorking::Filter.run_filters([filter_1, filter_2])
42
+ statuses.first.should be_success
43
+ statuses.last.should_not be_success
44
+ end
45
+
46
+ end