status-page 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: 5af2f060a08cd8558560479bcc2f1e051751c99d
4
+ data.tar.gz: 6eec97daa50456360a14693c331485b42cb69c40
5
+ SHA512:
6
+ metadata.gz: a505245fe787bd9c95f98e80d79f2ccefb84ed4a4d2502789a73b7c8c8a4962d6d4dc6f0873de37550959d197b65fa86734bdbc3bbabd3ccc74582a01434533e
7
+ data.tar.gz: ef15709301c85a6f58362bd7428ddd7ffa3cb8e0d3683434c1f6a8805c5da0e1c581c7cedcc2fc9e535c3d98a2dc56c69c1ca48f80a0563ecc7da8b5ddc94705
@@ -0,0 +1,111 @@
1
+ # status-page
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/status-page.svg)](http://badge.fury.io/rb/status-page) [![Build Status](https://travis-ci.org/rails-engine/status-page.svg)](https://travis-ci.org/rails-engine/status-page) [![Dependency Status](https://gemnasium.com/rails-engine/status-page.svg)](https://gemnasium.com/rails-engine/status-page) [![Coverage Status](https://coveralls.io/repos/rails-engine/status-page/badge.svg)](https://coveralls.io/r/rails-engine/status-page)
4
+
5
+ Mountable status page for your Rails application, to check (DB, Cache, Sidekiq, Redis, etc.).
6
+
7
+ Mounting this gem will add a '/status' route to your application, which can be used for health monitoring the application and its various services. The method will return an appropriate HTTP status as well as a JSON array representing the state of each service.
8
+
9
+ ## Example
10
+
11
+ <img src="https://cloud.githubusercontent.com/assets/5518/14341727/c12ccdee-fcc6-11e5-8c25-00324d0e9baa.png" />
12
+
13
+ ## Install
14
+
15
+ ```ruby
16
+ # Gemfile
17
+ gem 'status-page'
18
+ ```
19
+
20
+ Then run:
21
+
22
+ ```bash
23
+ $ bundle install
24
+ ```
25
+
26
+ ```ruby
27
+ # config/routes.rb
28
+ mount StatusPage::Engine, at: '/'
29
+ ```
30
+
31
+ ## Supported service services
32
+
33
+ The following services are currently supported:
34
+
35
+ * DB
36
+ * Cache
37
+ * Redis
38
+ * Sidekiq
39
+ * Resque
40
+
41
+ ## Configuration
42
+
43
+ ### Adding services
44
+
45
+ By default, only the database check is enabled. You can add more service services by explicitly enabling them via an initializer:
46
+
47
+ ```ruby
48
+ StatusPage.configure do
49
+ # Cache check status result 10 seconds
50
+ self.interval = 10
51
+ # Use service
52
+ self.use :database
53
+ self.use :cache
54
+ self.use :redis
55
+ self.use :sidekiq
56
+ end
57
+ ```
58
+
59
+ ### Adding a custom service
60
+
61
+ It's also possible to add custom health check services suited for your needs (of course, it's highly appreciated and encouraged if you'd contribute useful services to the project).
62
+
63
+ In order to add a custom service, you'd need to:
64
+
65
+ * Implement the `StatusPage::Services::Base` class and its `check!` method (a check is considered as failed if it raises an exception):
66
+
67
+ ```ruby
68
+ class CustomService < StatusPage::Services::Base
69
+ def check!
70
+ raise 'Oh oh!'
71
+ end
72
+ end
73
+ ```
74
+ * Add its class to the config:
75
+
76
+ ```ruby
77
+ StatusPage.configure do
78
+ self.add_custom_service(CustomProvider)
79
+ end
80
+ ```
81
+
82
+ ### Adding a custom error callback
83
+
84
+ If you need to perform any additional error handling (for example, for additional error reporting), you can configure a custom error callback:
85
+
86
+ ```ruby
87
+ StatusPage.configure do
88
+ self.error_callback = proc do |e|
89
+ logger.error "Health check failed with: #{e.message}"
90
+
91
+ Raven.capture_exception(e)
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Adding authentication credentials
97
+
98
+ By default, the `/status` endpoint is not authenticated and is available to any user. You can authenticate using HTTP Basic Auth by providing authentication credentials:
99
+
100
+ ```ruby
101
+ StatusPage.configure do
102
+ self.basic_auth_credentials = {
103
+ username: 'SECRET_NAME',
104
+ password: 'Shhhhh!!!'
105
+ }
106
+ end
107
+ ```
108
+
109
+ ## License
110
+
111
+ The MIT License (MIT)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'rspec/core/rake_task'
9
+ require 'rubocop/rake_task'
10
+
11
+ RSpec::Core::RakeTask.new('spec')
12
+ Bundler::GemHelper.install_tasks
13
+
14
+ task :default => :spec
15
+
16
+ RuboCop::RakeTask.new
@@ -0,0 +1,35 @@
1
+ module StatusPage
2
+ class StatusController < ActionController::Base
3
+ before_action :authenticate_with_basic_auth
4
+
5
+ def index
6
+ @statuses = statuses
7
+
8
+ respond_to do |format|
9
+ format.html
10
+ format.json {
11
+ render json: statuses
12
+ }
13
+ format.xml {
14
+ render xml: statuses
15
+ }
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def statuses
22
+ return @statuses if defined? @statuses
23
+ @statuses = StatusPage.check(request: request)
24
+ end
25
+
26
+ def authenticate_with_basic_auth
27
+ return true unless StatusPage.config.basic_auth_credentials
28
+
29
+ credentials = StatusPage.config.basic_auth_credentials
30
+ authenticate_or_request_with_http_basic do |name, password|
31
+ name == credentials[:username] && password == credentials[:password]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <title>Status</title>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width">
8
+ <style type="text/css" media="screen">
9
+ body {
10
+ line-height: 2rem;
11
+ font-size: 14px;
12
+ background-color: #f0f0f0;
13
+ margin: 0;
14
+ padding: 0;
15
+ color: #000;
16
+ text-align: center;
17
+ }
18
+
19
+ .container {
20
+ width: 960px;
21
+ margin: 20px auto;
22
+ text-align: left;
23
+ }
24
+
25
+ h1 {
26
+ font-weight: normal;
27
+ line-height: 2.8rem;
28
+ font-size: 30px;
29
+ letter-spacing: -1px;
30
+ text-align: center;
31
+ color: #333;
32
+ }
33
+
34
+ .container {
35
+ width: 960px;
36
+ margin:40px auto;
37
+ overflow: hidden;
38
+ }
39
+
40
+ .statuses {
41
+ background: #FFF;
42
+ width: 100%;
43
+ border-radius: 5px;
44
+ }
45
+ .statuses h1 { border-radius: 5px 5px 0 0; background: #f9f9f9; padding: 10px; border-bottom: 1px solid #eee;}
46
+ .statuses .status { font-size: 14px; border-bottom: 1px solid #eee; padding: 15px; }
47
+ .statuses .status:last-child { border-bottom: 0px; }
48
+ .statuses .name { font-size: 20px; margin-right: 20px; min-width: 100px; font-weight: bold; color: #555; }
49
+ .statuses .state { font-size: 14px; float: right; width: 80px; color: #45b81d; }
50
+ .statuses .message { color: #666; }
51
+ .statuses .timestamp { width: 130px; color: #999; }
52
+ .statuses .status-error .state { color: red; }
53
+ </style>
54
+ </head>
55
+
56
+ <body>
57
+ <div class="container">
58
+ <div class="statuses">
59
+ <h1>Status Page</h1>
60
+ <% @statuses[:results].each do |status| %>
61
+ <div class="status status-<%= status[:status].downcase %>">
62
+ <div class="status-heading">
63
+ <span class="name"><%= status[:name] %></span>
64
+ <span class="state"><%= status[:status] %></span>
65
+ </div>
66
+ <div class="message"><%= status[:message] %></div>
67
+ </div>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ </body>
@@ -0,0 +1,3 @@
1
+ StatusPage::Engine.routes.draw do
2
+ resources :status
3
+ end
@@ -0,0 +1,9 @@
1
+ # rubocop:disable Style/FileName
2
+
3
+ require 'status-page/version'
4
+ require 'status-page/engine'
5
+ require 'status-page/configuration'
6
+ require 'status-page/monitor'
7
+ require 'status-page/services/base'
8
+
9
+ # rubocop:enable Style/FileName
@@ -0,0 +1,33 @@
1
+ module StatusPage
2
+ class Configuration
3
+ attr_accessor :error_callback, :basic_auth_credentials, :interval
4
+ attr_reader :providers
5
+
6
+ def initialize
7
+ @providers = Set.new
8
+ @interval = 10
9
+ end
10
+
11
+ def use(service_name)
12
+ require "status-page/services/#{service_name}"
13
+ add_service("StatusPage::Services::#{service_name.capitalize}".constantize)
14
+ end
15
+
16
+ def add_custom_service(custom_service_class)
17
+ unless custom_service_class < StatusPage::Services::Base
18
+ raise ArgumentError.new 'custom provider class must implement '\
19
+ 'StatusPage::Services::Base'
20
+ end
21
+
22
+ add_service(custom_service_class)
23
+ end
24
+
25
+ private
26
+
27
+ def add_service(provider_class)
28
+ (@providers ||= Set.new) << provider_class
29
+
30
+ provider_class
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module StatusPage
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace StatusPage
4
+ end
5
+ end
@@ -0,0 +1,57 @@
1
+ module StatusPage
2
+ STATUSES = {
3
+ ok: 'OK',
4
+ error: 'ERROR'
5
+ }.freeze
6
+
7
+ class << self
8
+ def config
9
+ return @config if defined?(@config)
10
+ @config = Configuration.new
11
+ @config
12
+ end
13
+
14
+ def configure(&block)
15
+ config.instance_exec(&block)
16
+ end
17
+
18
+ def check(request: nil)
19
+ if config.interval > 0
20
+ if @cached_status && @cached_status[:timestamp] >= (config.interval || 5).seconds.ago
21
+ return @cached_status
22
+ end
23
+ end
24
+
25
+ providers = config.providers || []
26
+ results = providers.map { |provider| provider_result(provider, request) }
27
+
28
+ @cached_status = {
29
+ results: results,
30
+ status: results.all? { |result| result[:status] == STATUSES[:ok] } ? :ok : :service_unavailable,
31
+ timestamp: Time.now
32
+ }
33
+ @cached_status
34
+ end
35
+
36
+ private
37
+
38
+ def provider_result(provider, request)
39
+ monitor = provider.new(request: request)
40
+ monitor.check!
41
+
42
+ {
43
+ name: provider.service_name,
44
+ message: '',
45
+ status: STATUSES[:ok]
46
+ }
47
+ rescue => e
48
+ config.error_callback.call(e) if config.error_callback
49
+
50
+ {
51
+ name: provider.service_name,
52
+ message: e.message,
53
+ status: STATUSES[:error]
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ module StatusPage
2
+ module Services
3
+ class Base
4
+ attr_reader :request
5
+ cattr_accessor :config
6
+
7
+ def self.service_name
8
+ @name ||= name.demodulize
9
+ end
10
+
11
+ def self.configure
12
+ return unless configurable?
13
+
14
+ self.config ||= config_class.new
15
+
16
+ yield self.config if block_given?
17
+ end
18
+
19
+ def initialize(request: nil)
20
+ @request = request
21
+
22
+ self.class.configure
23
+ end
24
+
25
+ # @abstract
26
+ def check!
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def self.configurable?
31
+ config_class
32
+ end
33
+
34
+ # @abstract
35
+ def self.config_class
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ module StatusPage
2
+ module Services
3
+ class CacheException < StandardError; end
4
+
5
+ class Cache < Base
6
+ def check!
7
+ time = Time.now.to_s
8
+
9
+ Rails.cache.write(key, time)
10
+ fetched = Rails.cache.read(key)
11
+
12
+ raise "different values (now: #{time}, fetched: #{fetched})" if fetched != time
13
+ rescue Exception => e
14
+ raise CacheException.new(e.message)
15
+ end
16
+
17
+ private
18
+
19
+ def key
20
+ @key ||= ['status-cache', request.try(:remote_ip)].join(':')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module StatusPage
2
+ module Services
3
+ class DatabaseException < StandardError; end
4
+
5
+ class Database < Base
6
+ def check!
7
+ # Check connection to the DB:
8
+ ActiveRecord::Migrator.current_version
9
+ rescue Exception => e
10
+ raise DatabaseException.new(e.message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ require 'redis/namespace'
2
+
3
+ module StatusPage
4
+ module Services
5
+ class RedisException < StandardError; end
6
+
7
+ class Redis < Base
8
+ def check!
9
+ time = Time.now.to_s(:db)
10
+
11
+ redis = ::Redis.new
12
+ redis.set(key, time)
13
+ fetched = redis.get(key)
14
+
15
+ raise "different values (now: #{time}, fetched: #{fetched})" if fetched != time
16
+ rescue Exception => e
17
+ raise RedisException.new(e.message)
18
+ ensure
19
+ redis.client.disconnect
20
+ end
21
+
22
+ private
23
+
24
+ def key
25
+ @key ||= ['status-redis', request.try(:remote_ip)].join(':')
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ require 'resque'
2
+
3
+ module StatusPage
4
+ module Services
5
+ class ResqueException < StandardError; end
6
+
7
+ class Resque < Base
8
+ def check!
9
+ ::Resque.info
10
+ rescue Exception => e
11
+ raise ResqueException.new(e.message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,56 @@
1
+ require 'sidekiq/api'
2
+
3
+ module StatusPage
4
+ module Services
5
+ class SidekiqException < StandardError; end
6
+
7
+ class Sidekiq < Base
8
+ class Configuration
9
+ DEFAULT_LATENCY_TIMEOUT = 30
10
+
11
+ attr_accessor :latency
12
+
13
+ def initialize
14
+ @latency = DEFAULT_LATENCY_TIMEOUT
15
+ end
16
+ end
17
+
18
+ def check!
19
+ check_workers!
20
+ check_latency!
21
+ check_redis!
22
+ rescue Exception => e
23
+ raise SidekiqException.new(e.message)
24
+ end
25
+
26
+ private
27
+
28
+ class << self
29
+ private
30
+
31
+ def config_class
32
+ Configuration
33
+ end
34
+ end
35
+
36
+ def check_workers!
37
+ sidekiq_stats = ::Sidekiq::Stats.new
38
+ if sidekiq_stats.processes_size == 0
39
+ raise "Sidekiq alive processes is 0."
40
+ end
41
+ end
42
+
43
+ def check_latency!
44
+ latency = ::Sidekiq::Queue.new.latency
45
+
46
+ return unless latency > config.latency
47
+
48
+ raise "latency #{latency} is greater than #{config.latency}"
49
+ end
50
+
51
+ def check_redis!
52
+ ::Sidekiq.redis(&:info)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatusPage
4
+ VERSION = '0.1.1'
5
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: status-page
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Leonid Beder
8
+ - Jason Lee
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-04-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '4.2'
28
+ description: Health monitoring Rails plug-in, which checks various services (db, cache,
29
+ sidekiq, redis, etc.).
30
+ email:
31
+ - leonid.beder@gmail.com
32
+ - huacnlee@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - README.md
38
+ - Rakefile
39
+ - app/controllers/status_page/status_controller.rb
40
+ - app/views/status_page/status/index.html.erb
41
+ - config/routes.rb
42
+ - lib/status-page.rb
43
+ - lib/status-page/configuration.rb
44
+ - lib/status-page/engine.rb
45
+ - lib/status-page/monitor.rb
46
+ - lib/status-page/services/base.rb
47
+ - lib/status-page/services/cache.rb
48
+ - lib/status-page/services/database.rb
49
+ - lib/status-page/services/redis.rb
50
+ - lib/status-page/services/resque.rb
51
+ - lib/status-page/services/sidekiq.rb
52
+ - lib/status-page/version.rb
53
+ homepage: https://github.com/rails-engine/status-page
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 2.6.2
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Health monitoring Rails plug-in, which checks various services (db, cache,
77
+ sidekiq, redis, etc.)
78
+ test_files: []