tribune-is_it_working 1.0.9

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,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