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 +4 -4
- data/README.md +12 -2
- data/health-reporter.gemspec +3 -0
- data/lib/health_reporter/reporter.rb +39 -7
- data/lib/health_reporter/version.rb +1 -1
- data/spec/reporter.rb +80 -0
- data/spec/spec_helper.rb +1 -0
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cb4fa023528d34cc49ead890ceefdd3e4ea4c27
|
4
|
+
data.tar.gz: 917837548ae64725f7d6810f12359739beff2549
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
75
|
+
### Add service dependencies to check
|
76
76
|
|
77
|
-
|
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
|
|
data/health-reporter.gemspec
CHANGED
@@ -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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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)
|
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
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
|
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-
|
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
|