status-page 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []