wellness 1.0.2 → 2.0.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: be689a6ab96381d2df5fd2b0d54364b69a39b13b
4
- data.tar.gz: 1fd60250f3cf1078e12b980ce15e5b087e74b5c1
3
+ metadata.gz: 83d31bae5ada6170c0a1c9349df2e637c5cb2acf
4
+ data.tar.gz: 7e3d8a4b30dced90941b21c3c5c246e1e44c4a9a
5
5
  SHA512:
6
- metadata.gz: b1912e752de710339207d1690789a7d68d4efdb2080a3cae3601d69c306faba555ffb3bcc66c515998f9d75db901aff796707de7f383138cde87a65ed5c2afae
7
- data.tar.gz: 6099437bde358740b504951620a8ac2de008ec0d23b2e6e91b8edd425724beb129a88c560eb66e4081a8efc1427b41fc7b9ad597054380e3d81cd4caf6287260
6
+ metadata.gz: 2fb50394b50629a867c2cf8c1e4744850cfc4872bbb68944b9ba612711de715a3a0117c78b50f9decc0f0c137e2db2151bc09b6be5de4adb2306a873e34c1eae
7
+ data.tar.gz: 558bb60650dc8653edf5fd9eba21e122c4e1b271db52e0fc1ee8bb6eb8c11d7f2a588e2ab5c817c8748a5d5db3ad8689f02aa56e16ba7e30c615daba26adfc34
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.0.0-p353
1
+ 2.1.2
data/.travis.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.1.2
4
+ - 2.1.1
3
5
  - 2.0.0
4
- - 1.9.3
5
6
  notifications:
6
7
  irc: "chat.freenode.net#warmwaffles"
data/README.md CHANGED
@@ -14,22 +14,26 @@ However, you must require them when you need them. This is because they have
14
14
  external dependencies that need to be loaded, that your application may not
15
15
  necessarily have.
16
16
 
17
- ```rb
18
- # Within the configuration block
19
-
20
- system = Wellness::System.new('my-uber-duber-app')
21
- system.use(Wellness::Services::PostgresService, 'database', {
22
- host: ENV['POSTGRESQL_HOST'],
23
- port: ENV['POSTGRESQL_PORT'],
24
- database: ENV['POSTGRESQL_DATABASE'],
25
- user: ENV['POSTGRESQL_USERNAME'],
26
- password: ENV['POSTGRESQL_PASSWORD']
27
- })
28
- system.use(Wellness::Services::RedisService, 'redis', {
29
- host: ENV['REDIS_HOST']
30
- })
31
-
32
- config.middleware.insert_before('::ActiveRecord::QueryCache', 'Wellness::Middleware', system)
17
+ ```ruby
18
+ module MySuperCoolApplication
19
+ class Application < Rails::Application
20
+ system = Wellness::System.new('my-super-cool-app')
21
+ service = Wellness::Services::PostgresService.new({
22
+ host: ENV['POSTGRESQL_HOST'],
23
+ port: ENV['POSTGRESQL_PORT'],
24
+ database: ENV['POSTGRESQL_DATABASE'],
25
+ user: ENV['POSTGRESQL_USERNAME'],
26
+ password: ENV['POSTGRESQL_PASSWORD']
27
+ })
28
+ system.add_service('database', service, { critical: true })
29
+ service = Wellness::Services::RedisService.new({
30
+ host: ENV['REDIS_HOST']
31
+ })
32
+ system.add_service('redis', service, { critical: false})
33
+
34
+ config.middleware.insert_before('::ActiveRecord::QueryCache', 'Wellness::Middleware', system)
35
+ end
36
+ end
33
37
  ```
34
38
 
35
39
  ## Usage - Sinatra
@@ -37,28 +41,32 @@ config.middleware.insert_before('::ActiveRecord::QueryCache', 'Wellness::Middlew
37
41
  ```ruby
38
42
  require 'wellness'
39
43
 
40
- system = Wellness::System.new('my-uber-duber-app')
41
- system.use(Wellness::Services::PostgresService, 'database', {
44
+ system = Wellness::System.new('my-super-cool-app')
45
+ service = Wellness::Services::PostgresService.new({
42
46
  host: ENV['POSTGRESQL_HOST'],
43
47
  port: ENV['POSTGRESQL_PORT'],
44
48
  database: ENV['POSTGRESQL_DATABASE'],
45
49
  user: ENV['POSTGRESQL_USERNAME'],
46
50
  password: ENV['POSTGRESQL_PASSWORD']
47
51
  })
48
- system.use(Wellness::Services::RedisService, 'redis', {
52
+ system.add_service('database', service, { critical: true })
53
+ service = Wellness::Services::RedisService.new({
49
54
  host: ENV['REDIS_HOST']
50
55
  })
56
+ system.add_service('redis', service, { critical: false})
51
57
 
52
58
  use(Wellness::Middleware, system)
53
59
  ```
54
60
 
55
- ## Example Response
61
+ ## Example Responses
56
62
 
57
63
  ```json
58
64
  {
59
- "status":"UNHEALTHY",
60
- "details":{
61
-
65
+ "status": "UNHEALTHY",
66
+ "details": {
67
+ "git" : {
68
+ "revision" : "1234567"
69
+ }
62
70
  },
63
71
  "dependencies":{
64
72
  "database":{
@@ -89,58 +97,118 @@ use(Wellness::Middleware, system)
89
97
  }
90
98
  ```
91
99
 
100
+ ```json
101
+ {
102
+ "status": "HEALTHY",
103
+ "services": {
104
+ "postgresql": {
105
+ "status": "HEALTHY",
106
+ "details": { }
107
+ },
108
+ "mysql": {
109
+ "status": "HEALTHY",
110
+ "details": { }
111
+ }
112
+ },
113
+ "details": {
114
+ "git": {
115
+ "revision": "1234567"
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
92
121
  ## Custom Services
93
122
 
94
- Creating custom services is really easy. Always extend
95
- `Wellness::Services::Base`.
123
+ Creating a custom service is super easy. As long as the service responds to
124
+ `#call`, you are just fine. Passing a `lambda` or `Proc` is perfectly
125
+ acceptable.
96
126
 
97
- Once that is done, you must implement the `#check` method.
127
+ ### Requirements
98
128
 
99
- The parameters passed into the service at creation are stored under `#params`.
100
- Under no circumstances, should you ever modify the original parameters list at
101
- run time. It can lead to unintended consequences, and weird bugs.
129
+ This interface has a few requirements.
130
+
131
+ 1. A service **MUST** respond to `#call`
132
+ 2. A hash **MUST** be returned
133
+ 3. The hash returned **MUST** contain a `status` key at the root level.
134
+ 4. Status can **ONLY** be `HEALTHY`, `UNHEALTHY`, and `DEGRADED`
135
+ 5. All hash keys **MUST** be strings
136
+
137
+ ### Example
102
138
 
103
139
  ```ruby
104
- # Your custom service
105
- class MyCustomService < Wellness::Services::Base
106
- def check
107
- if params[:foo]
140
+ module MyServices
141
+ class MySQLService
142
+ def initialize(args={})
143
+ @connection_options = {
144
+ host: args[:host],
145
+ port: args[:port],
146
+ database: args[:database],
147
+ username: args[:username],
148
+ password: args[:password]
149
+ }
150
+ end
151
+
152
+ def healthy(details={})
108
153
  {
109
- 'status': 'HEALTHY'
154
+ 'status' => 'HEALTHY',
155
+ 'details' => details
110
156
  }
111
- else
157
+ end
158
+
159
+ def unhealthy(details={})
112
160
  {
113
- 'status': 'UNHEALTHY'
161
+ 'status' => 'UNHEALTHY',
162
+ 'details' => details
114
163
  }
115
164
  end
165
+
166
+ def call
167
+ connection = Mysql2::Client.new(@connection_options)
168
+ connection.ping ? healthy : unhealthy
169
+ rescue Mysql2::Error => error
170
+ unhealthy('error' => error.message)
171
+ end
116
172
  end
117
173
  end
118
174
 
119
- # Initialize the wellness system
120
175
  system = Wellness::System.new('my-app')
121
- system.use(MyCustomService, 'some service', {foo: 'bar'})
122
-
123
- # Load it into your rack
124
- use(Wellness::Middleware, system)
176
+ system.use('mysql', MyServices::MySQLService.new({
177
+ # ... configuration ...
178
+ }))
125
179
  ```
126
180
 
127
181
  ## Custom Details
128
182
 
183
+ Details are useful if you want to display information on an application like the
184
+ current git revision, or how many requests have been serviced by the application.
185
+
186
+ ### Requirements
187
+
188
+ 1. A detail **MUST** respond to `#call`
189
+ 2. A hash **MUST** be returned
190
+ 3. All hash keys **MUST** be strings
191
+
192
+ ### Example
193
+
129
194
  ```ruby
130
195
  # Your custom detail component
131
- class MyDetail < Wellness::Detail
132
- def check
133
- {
134
- 'foo' => 12,
135
- 'bar' => 31,
136
- 'qux' => options[:qux]
137
- }
196
+ module MyDetails
197
+ class GitRevisionDetail
198
+ def call
199
+ {
200
+ 'revision' => revision
201
+ }
202
+ end
203
+
204
+ def revision
205
+ `git rev-parse --short HEAD`.chomp
206
+ end
138
207
  end
139
208
  end
140
-
141
209
  # Initialize the wellness system
142
210
  system = Wellness::System.new('my-app')
143
- system.use(MyDetail, 'something', { qux: 9000 })
211
+ system.use('git', MyDetails::GitRevisionDetail.new)
144
212
 
145
213
  # Load it into your rack
146
214
  use(Wellness::Middleware, system)
data/Rakefile CHANGED
@@ -1,5 +1,26 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
2
 
4
- RSpec::Core::RakeTask.new(:spec)
5
- task :default => :spec
3
+ namespace :test do
4
+ task :env do
5
+ $LOAD_PATH.unshift('lib', 'spec', 'test')
6
+ end
7
+
8
+ desc 'Runs only the units in this project'
9
+ task :units => [:env] do
10
+ Dir.glob('./test/**/*_test.rb') { |f| require f }
11
+ end
12
+
13
+ desc 'Runs only the specs in this project'
14
+ task :specs => [:env] do
15
+ Dir.glob('./spec/**/*_spec.rb') { |f| require f }
16
+ end
17
+
18
+ desc 'Runs all of the tests within this project'
19
+ task :all => [:units, :specs]
20
+ end
21
+
22
+ desc 'Runs all of the tests within this project'
23
+ task :test => ['test:units']
24
+ task :spec => ['test:specs']
25
+
26
+ task(:default => 'test:all')
@@ -1,19 +1,18 @@
1
1
  module Wellness
2
2
  # The parent class of all details that need to run.
3
3
  #
4
+ # @deprecated This is simply here help with migrating
5
+ #
4
6
  # @author Matthew A. Johnston (warmwaffles)
5
7
  class Detail
6
- attr_reader :name, :options, :result
8
+ attr_reader :options
7
9
 
8
- def initialize(name, options={})
9
- @name = name
10
+ def initialize(options={})
10
11
  @options = options
11
- @result = {}
12
12
  end
13
13
 
14
14
  def call
15
- @result = self.check
16
- self
15
+ self.check
17
16
  end
18
17
 
19
18
  # @return [Hash]
@@ -21,4 +20,4 @@ module Wellness
21
20
  {}
22
21
  end
23
22
  end
24
- end
23
+ end
@@ -0,0 +1,64 @@
1
+ require 'json'
2
+
3
+ module Wellness
4
+ class DetailedReport
5
+ STATUSES = {
6
+ 'HEALTHY' => 200,
7
+ 'DEGRADED' => 500,
8
+ 'UNHEALTHY' => 500
9
+ }
10
+
11
+ def initialize(system)
12
+ @system = system
13
+ end
14
+
15
+ def call
16
+ non_criticals = []
17
+ criticals = []
18
+
19
+ services = {}
20
+ @system.services.each do |name, service|
21
+ services[name] = service.call
22
+
23
+ if service.critical?
24
+ criticals << services[name]['status']
25
+ else
26
+ non_criticals << services[name]['status']
27
+ end
28
+ end
29
+
30
+ details = {}
31
+ @system.details.each do |name, detail|
32
+ details[name] = detail.call
33
+ end
34
+
35
+ healthy = criticals.all? { |s| s == 'HEALTHY' }
36
+ degraded = non_criticals.any? { |s| s == 'UNHEALTHY' }
37
+
38
+ if healthy
39
+ if degraded
40
+ status = 'DEGRADED'
41
+ else
42
+ status = 'HEALTHY'
43
+ end
44
+ else
45
+ status = 'UNHEALTHY'
46
+ end
47
+
48
+ results = {
49
+ 'status' => status,
50
+ 'services' => services,
51
+ 'details' => details
52
+ }
53
+
54
+ render({json: results, status: STATUSES[status]})
55
+ end
56
+
57
+ def render(options={})
58
+ status = options[:status] || 200
59
+ response = JSON.dump(options[:json])
60
+
61
+ [status, { 'Content-Type' => 'application/json' }, [response]]
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,6 @@
1
+ require 'wellness/simple_report'
2
+ require 'wellness/detailed_report'
3
+
1
4
  module Wellness
2
5
  # This is to be put into the Rack environment.
3
6
  #
@@ -20,12 +23,12 @@ module Wellness
20
23
  def call(env)
21
24
  case env['PATH_INFO']
22
25
  when health_status_path
23
- @system.simple_check
26
+ Wellness::SimpleReport.new(@system).call
24
27
  when health_details_path
25
- @system.detailed_check
28
+ Wellness::DetailedReport.new(@system).call
26
29
  else
27
30
  @app.call(env)
28
31
  end
29
32
  end
30
33
  end
31
- end
34
+ end
@@ -0,0 +1,17 @@
1
+ module Wellness
2
+ # Wraps a callable object.
3
+ class Service
4
+ def initialize(callable, options={})
5
+ @callable = callable
6
+ @critical = options.fetch(:critical, true)
7
+ end
8
+
9
+ def critical?
10
+ !!@critical
11
+ end
12
+
13
+ def call
14
+ @callable.call
15
+ end
16
+ end
17
+ end
@@ -2,8 +2,6 @@ module Wellness
2
2
  module Services
3
3
  # @author Matthew A. Johnston
4
4
  class Base
5
- attr_reader :params, :result, :name
6
-
7
5
  # Load dependencies when the class is loaded. This makes putting requires
8
6
  # at the top of the file unnecessary. It plays nicely with the
9
7
  # auto loader.
@@ -11,39 +9,14 @@ module Wellness
11
9
  yield if block_given?
12
10
  end
13
11
 
14
- # @param params [Hash]
15
- def initialize(name, params={})
16
- @name = name
17
- @params = params
18
- @result = {}
19
- end
20
-
21
- # Returns true if the service is healthy, otherwise false
22
- # @return [TrueClass,FalseClass]
23
- def healthy?
24
- @result.fetch(:status, 'UNHEALTHY') == 'HEALTHY'
25
- end
26
-
27
- def passed_check
28
- warn('#passed_check has been deprecated')
12
+ def initialize(args={})
29
13
  end
30
14
 
31
- def failed_check
32
- warn('#failed_check has been deprecated')
33
- end
34
-
35
- # @return [Wellness::Services::Base]
36
15
  def call
37
- @result = self.check
38
- self
39
- end
40
-
41
- # @return [Hash]
42
- def check
43
16
  {
44
- status: 'UNHEALTHY'
17
+ 'status' => 'HEALTHY'
45
18
  }
46
19
  end
47
20
  end
48
21
  end
49
- end
22
+ end
@@ -2,15 +2,24 @@ require 'wellness/services/base'
2
2
 
3
3
  module Wellness
4
4
  module Services
5
- # @author Matthew A. Johnston
6
5
  class PostgresService < Base
7
6
  dependency do
8
- require('pg')
7
+ require 'pg'
8
+ end
9
+
10
+ def initialize(args={})
11
+ @connection_options = {
12
+ host: args[:host],
13
+ port: args[:port],
14
+ dbname: args[:database],
15
+ user: args[:user],
16
+ password: args[:password]
17
+ }
9
18
  end
10
19
 
11
20
  # @return [Hash]
12
- def check
13
- case PG::Connection.ping(connection_options)
21
+ def call
22
+ case PG::Connection.ping(@connection_options)
14
23
  when PG::Constants::PQPING_NO_ATTEMPT
15
24
  ping_failed('no attempt made to ping')
16
25
  when PG::Constants::PQPING_NO_RESPONSE
@@ -24,24 +33,13 @@ module Wellness
24
33
 
25
34
  private
26
35
 
27
- # @return [Hash]
28
- def connection_options
29
- {
30
- host: self.params[:host],
31
- port: self.params[:port],
32
- dbname: self.params[:database],
33
- user: self.params[:user],
34
- password: self.params[:password]
35
- }
36
- end
37
-
38
36
  # @param message [String] the reason it failed
39
37
  # @return [Hash]
40
38
  def ping_failed(message)
41
39
  {
42
- status: 'UNHEALTHY',
43
- details: {
44
- error: message
40
+ 'status' => 'UNHEALTHY',
41
+ 'details' => {
42
+ 'error' => message
45
43
  }
46
44
  }
47
45
  end
@@ -49,8 +47,8 @@ module Wellness
49
47
  # @return [Hash]
50
48
  def ping_successful
51
49
  {
52
- status: 'HEALTHY',
53
- details: {
50
+ 'status' => 'HEALTHY',
51
+ 'details' => {
54
52
  }
55
53
  }
56
54
  end
@@ -20,37 +20,31 @@ module Wellness
20
20
  'uptime_in_days'
21
21
  ]
22
22
 
23
- dependency do
24
- require('redis')
23
+ def initialize(args={})
24
+ @params = args
25
25
  end
26
26
 
27
27
  # @return [Hash]
28
- def check
29
- client = build_client
28
+ def call
29
+ client = Redis.new(@params)
30
30
  details = client.info.select { |k, _| KEYS.include?(k) }
31
- passed(details)
31
+ client.disconnect
32
+
33
+ {
34
+ 'status' => 'HEALTHY',
35
+ 'details' => details
36
+ }
32
37
  rescue Redis::BaseError => error
33
38
  failed(error)
34
39
  rescue Exception => error
35
40
  failed(error)
36
41
  end
37
42
 
38
- def build_client
39
- Redis.new(self.params)
40
- end
41
-
42
- def passed(details)
43
- {
44
- status: 'HEALTHY',
45
- details: details
46
- }
47
- end
48
-
49
43
  def failed(error)
50
44
  {
51
- status: 'UNHEALTHY',
52
- details: {
53
- error: error.message
45
+ 'status' => 'UNHEALTHY',
46
+ 'details' => {
47
+ 'error' => error.message
54
48
  }
55
49
  }
56
50
  end
@@ -6,40 +6,43 @@ module Wellness
6
6
  class SidekiqService < Base
7
7
  KEYS = %w(redis_stats uptime_in_days connected_clients used_memory_human used_memory_peak_human)
8
8
 
9
- dependency do
10
- require 'redis'
11
- require 'sidekiq'
9
+ def initialize(args={})
10
+ @params = args
12
11
  end
13
12
 
14
13
  # @return [Hash]
15
- def check
14
+ def call
16
15
  sidekiq_stats = Sidekiq::Stats.new
16
+
17
17
  queue = Sidekiq::Queue.new
18
- redis = Redis.new(self.params.fetch(:redis))
19
- redis_stats = redis.info.select { |k, _| KEYS.include?(k) }
18
+ redis = Redis.new(@params.fetch(:redis))
19
+
20
+ redis_stats = redis.info.select { |k, _| KEYS.include?(k) }
20
21
  workers_size = redis.scard('workers').to_i
21
22
 
23
+ redis.disconnect
24
+
22
25
  {
23
- status: 'HEALTHY',
24
- details: {
25
- processed: sidekiq_stats.processed,
26
- failed: sidekiq_stats.failed,
27
- busy: workers_size,
28
- enqueued: sidekiq_stats.enqueued,
29
- scheduled: sidekiq_stats.scheduled_size,
30
- retries: sidekiq_stats.retry_size,
31
- default_latency: queue.latency,
32
- redis: redis_stats
26
+ 'status' => 'HEALTHY',
27
+ 'details' => {
28
+ 'processed' => sidekiq_stats.processed,
29
+ 'failed' => sidekiq_stats.failed,
30
+ 'busy' => workers_size,
31
+ 'enqueued' => sidekiq_stats.enqueued,
32
+ 'scheduled' => sidekiq_stats.scheduled_size,
33
+ 'retries' => sidekiq_stats.retry_size,
34
+ 'default_latency' => queue.latency,
35
+ 'redis' => redis_stats
33
36
  }
34
37
  }
35
38
  rescue => error
36
39
  {
37
- status: 'UNHEALTHY',
38
- details: {
39
- error: error.message
40
+ 'status' => 'UNHEALTHY',
41
+ 'details' => {
42
+ 'error' => error.message
40
43
  }
41
44
  }
42
45
  end
43
46
  end
44
47
  end
45
- end
48
+ end