doctor-strange 0.1.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 782f2977af71550ceaaf9c022cb0ebe2a64c848a
4
+ data.tar.gz: 026347d2385308caf7fc6e94783c8cbcaa673622
5
+ SHA512:
6
+ metadata.gz: 38e0ca7bef140169251ad1ca321f8abdc286f6285e361582b10aa1158b551c6c1a170bd3ed7131d236335c0612a8dfb13a599b3b1e5e38dabeda867775fe4146
7
+ data.tar.gz: 4e263004f6940811be98d4d8cccf45121121d33b74cb933bff102cc3783592062c1a3bc5b54e77573e92078a563b2912c7f05257e2cad38c3195b37c3e8f31cf
@@ -0,0 +1,315 @@
1
+ # Doctor Strange
2
+
3
+ [![BuildStatus](https://circleci.com/gh/SiliconJungles/doctor-strange.svg?style=svg)](https://circleci.com/gh/SiliconJungles/doctor-strange)
4
+
5
+ Monitoring various services (db, cache, sidekiq/resque, email, redis, payment, etc).
6
+
7
+ ## Examples
8
+
9
+ ### HTML Status Page
10
+
11
+ ![alt example](/docs/screenshots/status-page.png "SiliconJungles Status Page")
12
+
13
+ ### JSON Response
14
+
15
+ ```bash
16
+ >> curl -s http://localhost:3000/check.json | json_pp
17
+ ```
18
+
19
+ ```json
20
+ {
21
+ "results": [{
22
+ "name": "Database",
23
+ "message": "",
24
+ "status": "OK"
25
+ }, {
26
+ "name": "Redis",
27
+ "message": "",
28
+ "status": "OK"
29
+ }, {
30
+ "name": "Sidekiq",
31
+ "message": "",
32
+ "status": "OK"
33
+ }, {
34
+ "name": "Email",
35
+ "message": "",
36
+ "status": "OK"
37
+ }, {
38
+ "name": "Payment",
39
+ "message": "",
40
+ "status": "OK"
41
+ }],
42
+ "status": "ok",
43
+ "timestamp": "2018-03-12 16:36:55 +0800"
44
+ }
45
+ ```
46
+
47
+ ### XML Response
48
+
49
+ ```bash
50
+ >> curl -s http://localhost:3000/check.xml
51
+ ```
52
+
53
+ ```xml
54
+ <?xml version="1.0" encoding="UTF-8"?>
55
+ <hash>
56
+ <results type="array">
57
+ <result>
58
+ <name>Database</name>
59
+ <message />
60
+ <status>OK</status>
61
+ </result>
62
+ <result>
63
+ <name>Redis</name>
64
+ <message />
65
+ <status>OK</status>
66
+ </result>
67
+ <result>
68
+ <name>Sidekiq</name>
69
+ <message />
70
+ <status>OK</status>
71
+ </result>
72
+ <result>
73
+ <name>Email</name>
74
+ <message />
75
+ <status>OK</status>
76
+ </result>
77
+ <result>
78
+ <name>Payment</name>
79
+ <message />
80
+ <status>OK</status>
81
+ </result>
82
+ </results>
83
+ <status type="symbol">ok</status>
84
+ <timestamp>2018-03-12 16:38:31 +0800</timestamp>
85
+ </hash>
86
+ ```
87
+
88
+ ## Setup
89
+
90
+ If you are using bundler add doctor-strange to your Gemfile:
91
+
92
+ ```ruby
93
+ gem 'doctor-strange'
94
+ ```
95
+
96
+ Then run:
97
+
98
+ ```bash
99
+ $ bundle install
100
+ ```
101
+
102
+ Otherwise install the gem:
103
+
104
+ ```bash
105
+ $ gem install doctor-strange
106
+ ```
107
+
108
+ ## Usage
109
+ You can mount this inside your app routes by adding this to config/routes.rb:
110
+
111
+ ```ruby
112
+ mount DoctorStrange::Engine, at: '/'
113
+ ```
114
+
115
+ ## Supported Service Providers
116
+ The following services are currently supported:
117
+
118
+ * DB
119
+ * Cache
120
+ * Redis
121
+ * Sidekiq
122
+ * Resque
123
+ * Stripe
124
+ * MailGun
125
+
126
+ ## Configuration
127
+
128
+ ### Adding AppName
129
+
130
+ The app name is `SiliconJungles` by default. You can change to your app name.
131
+
132
+ ```ruby
133
+ DoctorStrange.configure do |config|
134
+ config.app_name = "YOUR_APP_NAME"
135
+ end
136
+ ```
137
+
138
+ ### Adding Providers
139
+ By default, only the database check is enabled. You can add more service providers by explicitly enabling them via an initializer:
140
+
141
+ ```ruby
142
+ DoctorStrange.configure do |config|
143
+ config.cache
144
+ config.redis
145
+ config.sidekiq
146
+ # etc
147
+ end
148
+ ```
149
+
150
+ We believe that having the database check enabled by default is very important, but if you still want to disable it
151
+ (e.g., if you use a database that isn't covered by the check) - you can do that by calling the `no_database` method:
152
+
153
+ ```ruby
154
+ DoctorStrange.configure do |config|
155
+ config.no_database
156
+ end
157
+ ```
158
+
159
+ ### Provider Configuration
160
+
161
+ Some of the providers can also accept additional configuration:
162
+
163
+ ```ruby
164
+ # Sidekiq
165
+ DoctorStrange.configure do |config|
166
+ config.sidekiq.configure do |sidekiq_config|
167
+ sidekiq_config.latency = 3.hours
168
+ sidekiq_config.queue_size = 50
169
+ end
170
+ end
171
+ ```
172
+
173
+ ```ruby
174
+ # Redis
175
+ DoctorStrange.configure do |config|
176
+ config.redis.configure do |redis_config|
177
+ redis_config.connection = Redis.current # use your custom redis connection
178
+ redis_config.url = 'redis://user:pass@example.redis.com:90210/' # or URL
179
+ redis_config.max_used_memory = 200 # Megabytes
180
+ end
181
+ end
182
+ ```
183
+
184
+ ```ruby
185
+ # Email - MailGun by default
186
+ DoctorStrange.configure do |config|
187
+ config.email.configure do |email_config|
188
+ email_config.api_key = "abcd"
189
+ email_config.domain = "foo.bar"
190
+ end
191
+ end
192
+ ```
193
+
194
+ ```ruby
195
+ # Payment - Stripe by default
196
+ DoctorStrange.configure do |config|
197
+ config.payment.configure do |payment_config|
198
+ payment_config.api_key = "sk_live_xxxxx"
199
+ end
200
+ end
201
+ ```
202
+
203
+ The currently supported settings are:
204
+
205
+ #### Sidekiq
206
+
207
+ * `latency`: the latency (in seconds) of a queue (now - when the oldest job was enqueued) which is considered unhealthy (the default is 30 seconds, but larger processing queue should have a larger latency value).
208
+ * `queue_size`: the size (maximim) of a queue which is considered unhealthy (the default is 100).
209
+
210
+ #### Redis
211
+
212
+ * `url`: the url used to connect to your Redis instance - note, this is an optional configuration and will use the default connection if not specified
213
+ * `connection`: Use custom redis connection (e.g., `Redis.current`).
214
+ * `max_used_memory`: Set maximum expected memory usage of Redis in megabytes. Prevent memory leaks and keys overstore.
215
+
216
+ #### Email
217
+
218
+ - Default transaction email service for now is `MailGun`.
219
+
220
+ * `api_key`: the mail service api_key - this is a required value and will return unhealthy if not specified
221
+ * `domain`: the mail service domain value - this is a required value and will return unhealthy if not specified.
222
+
223
+ #### Payment
224
+
225
+ - Default payment service for now is `Stripe`.
226
+
227
+ * `api_key`: The Payment service's api_key - returns unhealthy if not specified or invalid.
228
+
229
+ ### Adding a Custom Provider
230
+ It's also possible to add custom health check providers suited for your needs (of course, it's highly appreciated and encouraged if you'd contribute useful providers to the project).
231
+
232
+ In order to add a custom provider, you'd need to:
233
+
234
+ * Implement the `DoctorStrange::Providers::Base` class and its `check!` method (a check is considered as failed if it raises an exception):
235
+
236
+ ```ruby
237
+ class CustomProvider < DoctorStrange::Providers::Base
238
+ def check!
239
+ raise 'Oh oh!'
240
+ end
241
+ end
242
+ ```
243
+ * Add its class to the configuration:
244
+
245
+ ```ruby
246
+ DoctorStrange.configure do |config|
247
+ config.add_custom_provider(CustomProvider)
248
+ end
249
+ ```
250
+
251
+ ### Adding a Custom Error Callback
252
+ If you need to perform any additional error handling (for example, for additional error reporting), you can configure a custom error callback:
253
+
254
+ ```ruby
255
+ DoctorStrange.configure do |config|
256
+ config.error_callback = proc do |e|
257
+ logger.error "Health check failed with: #{e.message}"
258
+
259
+ Raven.capture_exception(e)
260
+ end
261
+ end
262
+ ```
263
+
264
+ ### Adding Authentication Credentials
265
+ By default, the `/check` endpoint is not authenticated and is available to any user. You can authenticate using HTTP Basic Auth by providing authentication credentials:
266
+
267
+ ```ruby
268
+ DoctorStrange.configure do |config|
269
+ config.basic_auth_credentials = {
270
+ username: 'SECRET_NAME',
271
+ password: 'Shhhhh!!!'
272
+ }
273
+ end
274
+ ```
275
+
276
+ ### Adding Environment Variables
277
+ By default, environment variables is `nil`, so if you'd want to include additional parameters in the results JSON, all you need is to provide a `Hash` with your custom environment variables:
278
+
279
+ ```ruby
280
+ DoctorStrange.configure do |config|
281
+ config.environment_variables = {
282
+ build_number: 'BUILD_NUMBER',
283
+ git_sha: 'GIT_SHA'
284
+ }
285
+ end
286
+ ```
287
+
288
+ ## TODO
289
+
290
+ - [ ] Support another transaction Email services such as: SES, Mandrill by MailChimp, SendGird, AliCloud's DirectMail, etc.
291
+
292
+ - [ ] Support another payment services such as: Adyen, Braintree, etc.
293
+
294
+ ## License
295
+
296
+ The MIT License (MIT)
297
+
298
+ Copyright (c) 2018
299
+
300
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
301
+ this software and associated documentation files (the "Software"), to deal in
302
+ the Software without restriction, including without limitation the rights to
303
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
304
+ the Software, and to permit persons to whom the Software is furnished to do so,
305
+ subject to the following conditions:
306
+
307
+ The above copyright notice and this permission notice shall be included in all
308
+ copies or substantial portions of the Software.
309
+
310
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
311
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
312
+ FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
313
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
314
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
315
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new('spec')
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task default: :spec
10
+
11
+ RuboCop::RakeTask.new
@@ -0,0 +1,47 @@
1
+ module DoctorStrange
2
+ class HealthController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ if Rails.version.starts_with? '3'
6
+ before_filter :authenticate_with_basic_auth
7
+ else
8
+ before_action :authenticate_with_basic_auth
9
+ end
10
+
11
+ def check
12
+ @statuses = statuses
13
+ @app_name = DoctorStrange.configuration.app_name
14
+
15
+ respond_to do |format|
16
+ format.html
17
+ format.json do
18
+ render json: statuses.to_json
19
+ end
20
+ format.xml do
21
+ render xml: statuses.to_xml
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def statuses
29
+ res = DoctorStrange.check(request: request)
30
+ res.merge(env_vars)
31
+ end
32
+
33
+ def env_vars
34
+ v = DoctorStrange.configuration.environment_variables || {}
35
+ v.empty? ? {} : { environment_variables: v }
36
+ end
37
+
38
+ def authenticate_with_basic_auth
39
+ return true unless DoctorStrange.configuration.basic_auth_credentials
40
+
41
+ credentials = DoctorStrange.configuration.basic_auth_credentials
42
+ authenticate_or_request_with_http_basic do |name, password|
43
+ name == credentials[:username] && password == credentials[:password]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @app_name %>Status</title>
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta name="description" content="<%= @app_name %> Status">
8
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css">
9
+ </head>
10
+
11
+ <body>
12
+ <div class="container">
13
+ <h1 class="text-center"><%= @app_name %> Status Page</h1>
14
+ <table class="table table-hover table-bordered">
15
+ <thead>
16
+ <tr>
17
+ <th scope="col">#</th>
18
+ <th scope="col">Service</th>
19
+ <th scope="col">Status</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% @statuses[:results].each_with_index do |status, index| %>
24
+ <% class_name = status[:status].downcase == 'error' ? 'text-danger' : 'text-success' %>
25
+ <tr>
26
+ <th scope="row"><%= index + 1 %></th>
27
+ <td> <%= status[:name] %></td>
28
+ <td class="<%= class_name %>">
29
+ <p><%= status[:status] %></p>
30
+ <%= status[:message] %>
31
+ </td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+
37
+
38
+ <!-- <div class="statuses">
39
+ <h1><%= @app_name %> Status Page</h1>
40
+ <% @statuses[:results].each do |status| %>
41
+ <div class="status status-<%= status[:status].downcase %>">
42
+ <div class="status-heading">
43
+ <span class="name"><%= status[:name] %></span>
44
+ <span class="state"><%= status[:status] %></span>
45
+ </div>
46
+ <div class="message"><%= status[:message] %></div>
47
+ </div>
48
+ <% end %>
49
+ </div> -->
50
+ <div class="text-center">
51
+ Powered by <a href="https://github.com/SiliconJungles/doctor-strange.git" target="_blank">Doctor Strange - SiliconJungles Team</a>
52
+ </div>
53
+ </div>
54
+ </body>
@@ -0,0 +1,5 @@
1
+ DoctorStrange::Engine.routes.draw do
2
+ controller :health do
3
+ get :check
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ # rubocop:disable Naming/FileName
2
+
3
+ require 'doctor_strange/version'
4
+ require 'doctor_strange/engine'
5
+ require 'doctor_strange/configuration'
6
+ require 'doctor_strange/monitor'
7
+
8
+ # rubocop:enable Naming/FileName
@@ -0,0 +1,44 @@
1
+ module DoctorStrange
2
+ class Configuration
3
+ PROVIDERS = %i[cache database redis resque sidekiq email payment].freeze
4
+
5
+ DEFAULT_APP_NAME = "SiliconJungles".freeze
6
+
7
+ attr_accessor :error_callback, :basic_auth_credentials, :environment_variables, :app_name
8
+ attr_reader :providers
9
+
10
+ def initialize
11
+ database
12
+ @app_name = DEFAULT_APP_NAME
13
+ end
14
+
15
+ def no_database
16
+ @providers.delete(DoctorStrange::Providers::Database)
17
+ end
18
+
19
+ PROVIDERS.each do |provider_name|
20
+ define_method provider_name do |&_block|
21
+ require "doctor_strange/providers/#{provider_name}"
22
+
23
+ add_provider("DoctorStrange::Providers::#{provider_name.capitalize}".constantize)
24
+ end
25
+ end
26
+
27
+ def add_custom_provider(custom_provider_class)
28
+ unless custom_provider_class < DoctorStrange::Providers::Base
29
+ raise ArgumentError, 'custom provider class must implement '\
30
+ 'DoctorStrange::Providers::Base'
31
+ end
32
+
33
+ add_provider(custom_provider_class)
34
+ end
35
+
36
+ private
37
+
38
+ def add_provider(provider_class)
39
+ (@providers ||= Set.new) << provider_class
40
+
41
+ provider_class
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ module DoctorStrange
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace DoctorStrange
4
+ end
5
+ end
@@ -0,0 +1,51 @@
1
+ require 'doctor_strange/configuration'
2
+
3
+ module DoctorStrange
4
+ STATUSES = {
5
+ ok: 'OK',
6
+ error: 'ERROR'
7
+ }.freeze
8
+
9
+ extend self
10
+
11
+ attr_accessor :configuration
12
+
13
+ def configure
14
+ self.configuration ||= Configuration.new
15
+
16
+ yield configuration if block_given?
17
+ end
18
+
19
+ def check(request: nil)
20
+ results = configuration.providers.map { |provider| provider_result(provider, request) }
21
+
22
+ {
23
+ results: results,
24
+ status: results.all? { |res| res[:status] == STATUSES[:ok] } ? :ok : :service_unavailable,
25
+ timestamp: Time.now.to_s(:rfc2822)
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def provider_result(provider, request)
32
+ monitor = provider.new(request: request)
33
+ monitor.check!
34
+
35
+ {
36
+ name: provider.provider_name,
37
+ message: '',
38
+ status: STATUSES[:ok]
39
+ }
40
+ rescue StandardError => e
41
+ configuration.error_callback.call(e) if configuration.error_callback
42
+
43
+ {
44
+ name: provider.provider_name,
45
+ message: e.message,
46
+ status: STATUSES[:error]
47
+ }
48
+ end
49
+ end
50
+
51
+ DoctorStrange.configure
@@ -0,0 +1,39 @@
1
+ module DoctorStrange
2
+ module Providers
3
+ class Base
4
+ attr_reader :request
5
+ attr_accessor :configuration
6
+
7
+ def self.provider_name
8
+ @name ||= name.demodulize
9
+ end
10
+
11
+ def self.configure
12
+ return unless configurable?
13
+
14
+ @global_configuration = configuration_class.new
15
+
16
+ yield @global_configuration if block_given?
17
+ end
18
+
19
+ def initialize(request: nil)
20
+ @request = request
21
+
22
+ return unless self.class.configurable?
23
+ self.configuration = self.class.instance_variable_get('@global_configuration')
24
+ end
25
+
26
+ # @abstract
27
+ def check!
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def self.configurable?
32
+ configuration_class
33
+ end
34
+
35
+ # @abstract
36
+ def self.configuration_class; end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ require 'doctor_strange/providers/base'
2
+
3
+ module DoctorStrange
4
+ module Providers
5
+ class CacheException < StandardError; end
6
+
7
+ class Cache < Base
8
+ def check!
9
+ time = Time.now.to_s
10
+
11
+ Rails.cache.write(key, time)
12
+ fetched = Rails.cache.read(key)
13
+
14
+ raise "different values (now: #{time}, fetched: #{fetched})" if fetched != time
15
+ rescue StandardError => e
16
+ raise CacheException, e.message
17
+ end
18
+
19
+ private
20
+
21
+ def key
22
+ @key ||= ['health', request.try(:remote_ip)].join(':')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require 'doctor_strange/providers/base'
2
+
3
+ module DoctorStrange
4
+ module Providers
5
+ class DatabaseException < StandardError; end
6
+
7
+ class Database < Base
8
+ def check!
9
+ # Check connection to the DB:
10
+ ActiveRecord::Base.establish_connection # Establishes connection
11
+ ActiveRecord::Base.connection # Calls connection object
12
+ raise DatabaseException.new("Your database is not connected") unless ActiveRecord::Base.connected?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ require 'doctor_strange/providers/base'
2
+ require 'mailgun'
3
+
4
+ module DoctorStrange
5
+ module Providers
6
+ class EmailException < StandardError; end
7
+
8
+ class Email < Base
9
+ class Configuration
10
+ DEFAULT_SERVICE_NAME = "MailGun".freeze
11
+
12
+ attr_accessor :api_key, :domain, :service_name
13
+
14
+ def initialize
15
+ @service_name = DEFAULT_SERVICE_NAME
16
+ end
17
+ end
18
+
19
+ class << self
20
+ private
21
+
22
+ def configuration_class
23
+ ::DoctorStrange::Providers::Email::Configuration
24
+ end
25
+ end
26
+
27
+ def check!
28
+ check_required_values!
29
+ check_communication!
30
+ rescue StandardError => e
31
+ raise EmailException, e.message
32
+ end
33
+
34
+ private
35
+
36
+ def check_required_values!
37
+ raise "The api_key and domain are required" if configuration.api_key.empty? || configuration.domain.empty?
38
+ end
39
+
40
+ def check_communication!
41
+ mg_client = Mailgun::Client.new(configuration.api_key)
42
+ domainer = Mailgun::Domains.new(mg_client)
43
+ domainer.list.pluck("name").include? configuration.domain
44
+ rescue Mailgun::CommunicationError
45
+ raise "Cannot communicate to Mailgun"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ require 'doctor_strange/providers/base'
2
+ require 'stripe'
3
+
4
+ module DoctorStrange
5
+ module Providers
6
+ class PaymentException < StandardError; end
7
+
8
+ class Payment < Base
9
+ class Configuration
10
+ DEFAULT_SERVICE_NAME = "Stripe".freeze
11
+
12
+ attr_accessor :api_key, :service_name
13
+
14
+ def initialize
15
+ @service_name = DEFAULT_SERVICE_NAME
16
+ end
17
+ end
18
+
19
+ class << self
20
+ private
21
+
22
+ def configuration_class
23
+ ::DoctorStrange::Providers::Payment::Configuration
24
+ end
25
+ end
26
+
27
+ def check!
28
+ check_required_values!
29
+ check_communication!
30
+ rescue Exception => e
31
+ raise PaymentException.new(e.message)
32
+ end
33
+
34
+ private
35
+
36
+ def check_required_values!
37
+ raise "The api_key is required" if configuration.api_key.empty?
38
+ end
39
+
40
+ def check_communication!
41
+ Stripe.api_key = configuration.api_key
42
+ Stripe::Customer.list(limit: 1)
43
+ rescue Stripe::AuthenticationError
44
+ raise "Invalid API Key provided"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,77 @@
1
+ require 'doctor_strange/providers/base'
2
+
3
+ module DoctorStrange
4
+ module Providers
5
+ class RedisException < StandardError; end
6
+
7
+ class Redis < Base
8
+ class Configuration
9
+ DEFAULT_URL = nil
10
+
11
+ attr_accessor :url, :connection, :max_used_memory
12
+
13
+ def initialize
14
+ @url = DEFAULT_URL
15
+ end
16
+ end
17
+
18
+ class << self
19
+ private
20
+
21
+ def configuration_class
22
+ ::DoctorStrange::Providers::Redis::Configuration
23
+ end
24
+ end
25
+
26
+ def check!
27
+ check_values!
28
+ check_max_used_memory!
29
+ rescue Exception => e
30
+ raise RedisException.new(e.message)
31
+ ensure
32
+ redis.close
33
+ end
34
+
35
+ private
36
+
37
+ def check_values!
38
+ time = Time.now.to_s(:rfc2822)
39
+
40
+ redis.set(key, time)
41
+ fetched = redis.get(key)
42
+
43
+ raise "different values (now: #{time}, fetched: #{fetched})" if fetched != time
44
+ end
45
+
46
+ def check_max_used_memory!
47
+ return unless configuration.max_used_memory
48
+ return if used_memory_mb <= configuration.max_used_memory
49
+
50
+ raise "#{used_memory_mb}Mb memory using is higher than #{configuration.max_used_memory}Mb maximum expected"
51
+ end
52
+
53
+ def key
54
+ @key ||= ['health', request.try(:remote_ip)].join(':')
55
+ end
56
+
57
+ def redis
58
+ @redis =
59
+ if configuration.connection
60
+ configuration.connection
61
+ elsif configuration.url
62
+ ::Redis.new(url: configuration.url)
63
+ else
64
+ ::Redis.new
65
+ end
66
+ end
67
+
68
+ def bytes_to_megabytes(bytes)
69
+ (bytes.to_f / 1024 / 1024).round
70
+ end
71
+
72
+ def used_memory_mb
73
+ bytes_to_megabytes(redis.info['used_memory'])
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ require 'doctor_strange/providers/base'
2
+ require 'resque'
3
+
4
+ module DoctorStrange
5
+ module Providers
6
+ class ResqueException < StandardError; end
7
+
8
+ class Resque < Base
9
+ def check!
10
+ ::Resque.info
11
+ rescue Exception => e
12
+ raise ResqueException.new(e.message)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
1
+ require 'doctor_strange/providers/base'
2
+ require 'sidekiq/api'
3
+
4
+ module DoctorStrange
5
+ module Providers
6
+ class SidekiqException < StandardError; end
7
+
8
+ class Sidekiq < Base
9
+ class Configuration
10
+ DEFAULT_LATENCY_TIMEOUT = 30
11
+ DEFAULT_QUEUES_SIZE = 100
12
+
13
+ attr_accessor :latency, :queue_size
14
+
15
+ def initialize
16
+ @latency = DEFAULT_LATENCY_TIMEOUT
17
+ @queue_size = DEFAULT_QUEUES_SIZE
18
+ end
19
+ end
20
+
21
+ def check!
22
+ check_workers!
23
+ check_processes!
24
+ check_latency!
25
+ check_queue_size!
26
+ check_redis!
27
+ rescue Exception => e
28
+ raise SidekiqException, e.message
29
+ end
30
+
31
+ private
32
+
33
+ class << self
34
+ private
35
+
36
+ def configuration_class
37
+ ::DoctorStrange::Providers::Sidekiq::Configuration
38
+ end
39
+ end
40
+
41
+ def check_workers!
42
+ ::Sidekiq::Workers.new.size
43
+ end
44
+
45
+ def check_processes!
46
+ sidekiq_stats = ::Sidekiq::Stats.new
47
+ return unless sidekiq_stats.processes_size.zero?
48
+
49
+ raise 'Sidekiq alive processes number is 0!'
50
+ end
51
+
52
+ def check_latency!
53
+ latency = queue.latency
54
+
55
+ return unless latency > configuration.latency
56
+
57
+ raise "latency #{latency} is greater than #{configuration.latency}"
58
+ end
59
+
60
+ def check_queue_size!
61
+ size = queue.size
62
+
63
+ return unless size > configuration.queue_size
64
+
65
+ raise "queue size #{size} is greater than #{configuration.queue_size}"
66
+ end
67
+
68
+ def check_redis!
69
+ if ::Sidekiq.respond_to?(:redis_info)
70
+ ::Sidekiq.redis_info
71
+ else
72
+ ::Sidekiq.redis(&:info)
73
+ end
74
+ end
75
+
76
+ private def queue
77
+ @queue ||= ::Sidekiq::Queue.new
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoctorStrange
4
+ VERSION = '0.1.1'
5
+ end
metadata ADDED
@@ -0,0 +1,402 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: doctor-strange
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - SiliconJungles
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: appraisal
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.2.0
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.2'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.2.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: capybara
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.18'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.18.0
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.18'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.18.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: capybara-screenshot
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.18
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.0'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.0.18
87
+ - !ruby/object:Gem::Dependency
88
+ name: coveralls
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '0.8'
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.8.21
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.8'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 0.8.21
107
+ - !ruby/object:Gem::Dependency
108
+ name: database_cleaner
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '1.6'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 1.6.2
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.6'
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 1.6.2
127
+ - !ruby/object:Gem::Dependency
128
+ name: mailgun-ruby
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '1.1'
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 1.1.9
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '1.1'
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 1.1.9
147
+ - !ruby/object:Gem::Dependency
148
+ name: pry
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: 0.11.3
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: 0.11.3
161
+ - !ruby/object:Gem::Dependency
162
+ name: rake
163
+ requirement: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '0.8'
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: 0.8.7
171
+ type: :development
172
+ prerelease: false
173
+ version_requirements: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - "~>"
176
+ - !ruby/object:Gem::Version
177
+ version: '0.8'
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 0.8.7
181
+ - !ruby/object:Gem::Dependency
182
+ name: rediska
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '0.5'
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: 0.5.0
191
+ type: :development
192
+ prerelease: false
193
+ version_requirements: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - "~>"
196
+ - !ruby/object:Gem::Version
197
+ version: '0.5'
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: 0.5.0
201
+ - !ruby/object:Gem::Dependency
202
+ name: resque
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: '1.27'
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: 1.27.4
211
+ type: :development
212
+ prerelease: false
213
+ version_requirements: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - "~>"
216
+ - !ruby/object:Gem::Version
217
+ version: '1.27'
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: 1.27.4
221
+ - !ruby/object:Gem::Dependency
222
+ name: rspec-rails
223
+ requirement: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - "~>"
226
+ - !ruby/object:Gem::Version
227
+ version: '3.7'
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: 3.7.2
231
+ type: :development
232
+ prerelease: false
233
+ version_requirements: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - "~>"
236
+ - !ruby/object:Gem::Version
237
+ version: '3.7'
238
+ - - ">="
239
+ - !ruby/object:Gem::Version
240
+ version: 3.7.2
241
+ - !ruby/object:Gem::Dependency
242
+ name: rspec_junit_formatter
243
+ requirement: !ruby/object:Gem::Requirement
244
+ requirements:
245
+ - - "~>"
246
+ - !ruby/object:Gem::Version
247
+ version: 0.3.0
248
+ type: :development
249
+ prerelease: false
250
+ version_requirements: !ruby/object:Gem::Requirement
251
+ requirements:
252
+ - - "~>"
253
+ - !ruby/object:Gem::Version
254
+ version: 0.3.0
255
+ - !ruby/object:Gem::Dependency
256
+ name: rubocop
257
+ requirement: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - "~>"
260
+ - !ruby/object:Gem::Version
261
+ version: '0.5'
262
+ type: :development
263
+ prerelease: false
264
+ version_requirements: !ruby/object:Gem::Requirement
265
+ requirements:
266
+ - - "~>"
267
+ - !ruby/object:Gem::Version
268
+ version: '0.5'
269
+ - !ruby/object:Gem::Dependency
270
+ name: sidekiq
271
+ requirement: !ruby/object:Gem::Requirement
272
+ requirements:
273
+ - - "~>"
274
+ - !ruby/object:Gem::Version
275
+ version: '3.0'
276
+ type: :development
277
+ prerelease: false
278
+ version_requirements: !ruby/object:Gem::Requirement
279
+ requirements:
280
+ - - "~>"
281
+ - !ruby/object:Gem::Version
282
+ version: '3.0'
283
+ - !ruby/object:Gem::Dependency
284
+ name: spork
285
+ requirement: !ruby/object:Gem::Requirement
286
+ requirements:
287
+ - - "~>"
288
+ - !ruby/object:Gem::Version
289
+ version: 0.9.2
290
+ type: :development
291
+ prerelease: false
292
+ version_requirements: !ruby/object:Gem::Requirement
293
+ requirements:
294
+ - - "~>"
295
+ - !ruby/object:Gem::Version
296
+ version: 0.9.2
297
+ - !ruby/object:Gem::Dependency
298
+ name: sqlite3
299
+ requirement: !ruby/object:Gem::Requirement
300
+ requirements:
301
+ - - "~>"
302
+ - !ruby/object:Gem::Version
303
+ version: '1.3'
304
+ - - ">="
305
+ - !ruby/object:Gem::Version
306
+ version: 1.3.13
307
+ type: :development
308
+ prerelease: false
309
+ version_requirements: !ruby/object:Gem::Requirement
310
+ requirements:
311
+ - - "~>"
312
+ - !ruby/object:Gem::Version
313
+ version: '1.3'
314
+ - - ">="
315
+ - !ruby/object:Gem::Version
316
+ version: 1.3.13
317
+ - !ruby/object:Gem::Dependency
318
+ name: stripe
319
+ requirement: !ruby/object:Gem::Requirement
320
+ requirements:
321
+ - - "~>"
322
+ - !ruby/object:Gem::Version
323
+ version: '3.11'
324
+ - - ">="
325
+ - !ruby/object:Gem::Version
326
+ version: 3.11.0
327
+ type: :development
328
+ prerelease: false
329
+ version_requirements: !ruby/object:Gem::Requirement
330
+ requirements:
331
+ - - "~>"
332
+ - !ruby/object:Gem::Version
333
+ version: '3.11'
334
+ - - ">="
335
+ - !ruby/object:Gem::Version
336
+ version: 3.11.0
337
+ - !ruby/object:Gem::Dependency
338
+ name: timecop
339
+ requirement: !ruby/object:Gem::Requirement
340
+ requirements:
341
+ - - "~>"
342
+ - !ruby/object:Gem::Version
343
+ version: 0.9.1
344
+ type: :development
345
+ prerelease: false
346
+ version_requirements: !ruby/object:Gem::Requirement
347
+ requirements:
348
+ - - "~>"
349
+ - !ruby/object:Gem::Version
350
+ version: 0.9.1
351
+ description: A Rails plug-in, which checks various services (db, cache, sidekiq, redis,
352
+ stripe, mailgun, etc.).
353
+ email:
354
+ - developers@siliconjungles.com
355
+ executables: []
356
+ extensions: []
357
+ extra_rdoc_files: []
358
+ files:
359
+ - README.md
360
+ - Rakefile
361
+ - app/controllers/doctor_strange/health_controller.rb
362
+ - app/views/doctor_strange/health/check.html.erb
363
+ - config/routes.rb
364
+ - lib/doctor-strange.rb
365
+ - lib/doctor_strange/configuration.rb
366
+ - lib/doctor_strange/engine.rb
367
+ - lib/doctor_strange/monitor.rb
368
+ - lib/doctor_strange/providers/base.rb
369
+ - lib/doctor_strange/providers/cache.rb
370
+ - lib/doctor_strange/providers/database.rb
371
+ - lib/doctor_strange/providers/email.rb
372
+ - lib/doctor_strange/providers/payment.rb
373
+ - lib/doctor_strange/providers/redis.rb
374
+ - lib/doctor_strange/providers/resque.rb
375
+ - lib/doctor_strange/providers/sidekiq.rb
376
+ - lib/doctor_strange/version.rb
377
+ homepage: https://github.com/SiliconJungles/doctor-strange
378
+ licenses:
379
+ - MIT
380
+ metadata: {}
381
+ post_install_message:
382
+ rdoc_options: []
383
+ require_paths:
384
+ - lib
385
+ required_ruby_version: !ruby/object:Gem::Requirement
386
+ requirements:
387
+ - - ">="
388
+ - !ruby/object:Gem::Version
389
+ version: '0'
390
+ required_rubygems_version: !ruby/object:Gem::Requirement
391
+ requirements:
392
+ - - ">="
393
+ - !ruby/object:Gem::Version
394
+ version: '0'
395
+ requirements: []
396
+ rubyforge_project:
397
+ rubygems_version: 2.6.13
398
+ signing_key:
399
+ specification_version: 4
400
+ summary: A Rails plug-in, which checks various services (db, cache, sidekiq, redis,
401
+ stripe, mailgun, etc.)
402
+ test_files: []