health-reporter 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 155d97c5b4cc48d064e2d0c3ba40c19094a6c700
4
- data.tar.gz: 2e1bc68f9c30819cd1fd586ce1a7f01552e038dc
3
+ metadata.gz: 0cb4fa023528d34cc49ead890ceefdd3e4ea4c27
4
+ data.tar.gz: 917837548ae64725f7d6810f12359739beff2549
5
5
  SHA512:
6
- metadata.gz: 049f9b1956fccdd9d144bcb087e159b8555e2e861d239a0f32929209ce65c88426586fa5d2b18647656f02e50e4be0e08e1ba1d426d30427f36b6dd007bdd9a8
7
- data.tar.gz: 1a538e5844bff5fd7c8ac3a0e29d1814e86884ffb03f040959dfdc731944865e64960237453c9f10ef6d3eb73581cec4f276980e0f601c42f0d9b836373bdb46
6
+ metadata.gz: ab251e2c390fbcd452c91fd626a7f9cb0064495bb1c1474657e0840206a9a1ee20d03846b1559e51d989c5c2434a4ff06d48586f2a84a072c10ba5b9ec477896
7
+ data.tar.gz: 9ecfa361f55d9663da8475b996c8e17fbf5d9a6a98fad32e13c0b4a123bfac4acd30fda40648b90140aec044d5a429e43426cb29d98433b9f8d15259aed93506
data/README.md CHANGED
@@ -72,9 +72,19 @@ In the controller/model of the health check you simply call the following and ba
72
72
  => false
73
73
  ```
74
74
 
75
- ## Future
75
+ ### Add service dependencies to check
76
76
 
77
- ### Add dependency registration and calling
77
+ In a microservices environment the health of a service is also determined by the health of other services it is reaching out to during normal operation. This gem allows you to register those dependency services to also be checked. The dependencies are checked along with the service self-check. The combined health state will be cached. Therefore whilst the cache is still valid, the dependencies will also not be rechecked.
78
+
79
+ ```ruby
80
+ HealthReporter.register_dependency(url: 'https://hardware-store/status')
81
+ ```
82
+
83
+ Or with specific status code and timeout configuration:
84
+
85
+ ```ruby
86
+ HealthReporter.register_dependency(url: 'https://hardware-store/status', code: 200, timeout: 3)
87
+ ```
78
88
 
79
89
  ## Contributing
80
90
 
@@ -19,6 +19,9 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ["lib"]
21
21
 
22
+ spec.add_dependency 'faraday', '~> 0.14.0'
23
+
24
+ spec.add_development_dependency 'webmock', '~> 2.1'
22
25
  spec.add_development_dependency 'timecop', '~> 0.8.0'
23
26
  spec.add_development_dependency 'bundler', '~> 1.3'
24
27
  spec.add_development_dependency 'rake', '~> 10.0'
@@ -1,4 +1,6 @@
1
1
  require 'singleton'
2
+ require 'uri'
3
+ require 'faraday'
2
4
 
3
5
  class HealthReporter
4
6
  include Singleton
@@ -30,16 +32,23 @@ class HealthReporter
30
32
  @@self_test = lambda{ true }
31
33
  @@unhealthy_cache_ttl = 30
32
34
  @@healthy_cache_ttl = 60
35
+ @@dependencies = {}
33
36
  @@last_check_time = nil
34
37
  @@healthy = nil #Initialized as nil so that first call will set it
35
38
  @@semaphore = Mutex.new
36
39
 
37
- # TODO
38
- # def register_dependency_check(url:, code: 200)
39
- # raise "Configured URL #{url} is invalid" unless url =~ URI::regexp
40
- #
41
- # dependencies[]
42
- # end
40
+ def self.clear_dependencies
41
+ @@dependencies = {}
42
+ end
43
+
44
+ def self.dependencies
45
+ @@dependencies
46
+ end
47
+
48
+ def self.register_dependency(url:, code: 200, timeout: 2)
49
+ raise "Configured URL #{url} is invalid" unless url =~ URI::regexp
50
+ dependencies[url] = { :code => code, :timeout => timeout }
51
+ end
43
52
 
44
53
  def self.healthy?
45
54
  @@semaphore.synchronize {
@@ -51,8 +60,31 @@ class HealthReporter
51
60
  private
52
61
 
53
62
  def self.perform_health_check
54
- @@healthy = sanitize(@@self_test.call)
55
63
  @@last_check_time = Time.now
64
+ @@healthy = sanitize(@@self_test.call)
65
+ check_dependencies if @@healthy
66
+ end
67
+
68
+ def self.check_dependencies
69
+ @@dependencies.each { |url, configuration|
70
+ check_dependency(url: url, configuration: configuration)
71
+ }
72
+ @@healthy = true
73
+ end
74
+
75
+ def self.check_dependency(url:, configuration:)
76
+ conn = Faraday.new(:url => url)
77
+ response = conn.get do |request|
78
+ request.options.timeout = configuration[:timeout]
79
+ request.options.open_timeout = configuration[:timeout]
80
+ end
81
+
82
+ unless response.status == configuration[:code]
83
+ raise "Response expected to be #{configuration[:code]} but is #{response.status}"
84
+ end
85
+ rescue => exception
86
+ @@healthy = false
87
+ raise "Dependency <#{url}> failed check due to #{exception.class}: #{exception.message}"
56
88
  end
57
89
 
58
90
  def self.sanitize(result)
@@ -1,3 +1,3 @@
1
1
  class HealthReporter
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
data/spec/reporter.rb CHANGED
@@ -13,6 +13,7 @@ describe HealthReporter do
13
13
  subject.self_test = lambda{ true }
14
14
  subject.class_variable_set(:@@last_check_time, nil)
15
15
  subject.class_variable_set(:@@healthy, nil)
16
+ subject.clear_dependencies
16
17
  Timecop.return
17
18
  reset_lambda_runner_spy
18
19
  end
@@ -41,6 +42,38 @@ describe HealthReporter do
41
42
  subject.unhealthy_cache_ttl = 5
42
43
  expect(subject.unhealthy_cache_ttl).to eq 5
43
44
  end
45
+
46
+ it 'remembers when you add a dependencies' do
47
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
48
+ expect(subject.dependencies).to eq({
49
+ 'https://hardware-store/status' => { :code => 123, :timeout => 1 }
50
+ })
51
+ end
52
+
53
+ it 'validates the urls of the dependencies during registration' do
54
+ expect{subject.register_dependency(url: 'no-valid-url')}.to raise_error RuntimeError, "Configured URL no-valid-url is invalid"
55
+ end
56
+
57
+ it 'adds dependencies without removing the dependencies already registered' do
58
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
59
+ subject.register_dependency(url: 'https://grocery-store/status', code: 123, timeout: 1)
60
+ expect(subject.dependencies).to eq({
61
+ 'https://hardware-store/status' => { :code => 123, :timeout => 1 },
62
+ 'https://grocery-store/status' => { :code => 123, :timeout => 1 }
63
+ })
64
+ end
65
+
66
+ it 'does not duplicate similar dependency urls' do
67
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
68
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
69
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
70
+ subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
71
+ subject.register_dependency(url: 'https://grocery-store/status', code: 123, timeout: 1)
72
+ expect(subject.dependencies).to eq({
73
+ 'https://hardware-store/status' => { :code => 123, :timeout => 1 },
74
+ 'https://grocery-store/status' => { :code => 123, :timeout => 1 }
75
+ })
76
+ end
44
77
  end
45
78
 
46
79
  context 'when exercising self-test lambda' do
@@ -196,4 +229,51 @@ describe HealthReporter do
196
229
  end
197
230
  end
198
231
  end
232
+
233
+ context 'when checking dependencies' do
234
+ context 'when there are no dependencies registered' do
235
+ it 'only performs the self-test' do
236
+ subject.self_test = spy_lambda_returning_false
237
+ expect(subject.healthy?).to be false
238
+ expect(spy_lambda_was_run?).to eq true
239
+ end
240
+ end
241
+
242
+ context 'when there are multiple dependencies registered' do
243
+ before(:each) do
244
+ subject.register_dependency(url: 'https://hardware-store/status')
245
+ subject.register_dependency(url: 'https://grocery-store/status')
246
+ subject.self_test = spy_lambda_returning_true
247
+ end
248
+
249
+ it 'performs the self-test and checks all dependencies' do
250
+ stub_request(:get, "https://hardware-store/status").to_return(:status => 200, :body => "", :headers => {})
251
+ stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
252
+ expect(subject.healthy?).to be true
253
+ expect(spy_lambda_was_run?).to eq true
254
+ end
255
+
256
+ it 'indicates healthy if all of the dependencies are healthy' do
257
+ stub_request(:get, "https://hardware-store/status").to_return(:status => 200, :body => "", :headers => {})
258
+ stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
259
+ expect(subject.healthy?).to be true
260
+ expect(spy_lambda_was_run?).to eq true
261
+ end
262
+
263
+ it 'raises a detailed exception indicating why a dependency was determined to be unhealthy state was uncached' do
264
+ stub_request(:get, "https://hardware-store/status").to_return(:status => 500, :body => "", :headers => {})
265
+ stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
266
+ expect{subject.healthy?}.to raise_error RuntimeError, "Dependency <https://hardware-store/status> failed check due to RuntimeError: Response expected to be 200 but is 500"
267
+ end
268
+
269
+ it 'indicates cached unhealthy state if it is unhealthy because a dependency was unhealthy' do
270
+ stub_request(:get, "https://hardware-store/status").to_return(:status => 500, :body => "", :headers => {})
271
+ stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
272
+ expect{subject.healthy?}.to raise_error RuntimeError, "Dependency <https://hardware-store/status> failed check due to RuntimeError: Response expected to be 200 but is 500"
273
+ reset_lambda_runner_spy
274
+ expect(subject.healthy?).to be false
275
+ expect(spy_lambda_was_run?).to eq false
276
+ end
277
+ end
278
+ end
199
279
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'simplecov'
2
2
  require 'simplecov-rcov'
3
3
  require 'timecop'
4
+ require 'webmock/rspec'
4
5
 
5
6
  SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
6
7
  SimpleCov.start do
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: health-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Barney de Villiers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-06 00:00:00.000000000 Z
11
+ date: 2018-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.14.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.14.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: timecop
15
43
  requirement: !ruby/object:Gem::Requirement