hcheck 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +98 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +9 -0
- data/exe/hcheck +18 -0
- data/hcheck.gemspec +56 -0
- data/hcheck.sample.yml +47 -0
- data/lib/hcheck.rb +54 -0
- data/lib/hcheck/application.rb +42 -0
- data/lib/hcheck/application/helpers/responders.rb +55 -0
- data/lib/hcheck/application/views/error.haml +26 -0
- data/lib/hcheck/application/views/index.haml +38 -0
- data/lib/hcheck/checks/memcached.rb +22 -0
- data/lib/hcheck/checks/mongodb.rb +26 -0
- data/lib/hcheck/checks/mysql.rb +22 -0
- data/lib/hcheck/checks/ping.rb +38 -0
- data/lib/hcheck/checks/postgresql.rb +24 -0
- data/lib/hcheck/checks/rabbitmq.rb +29 -0
- data/lib/hcheck/checks/redis.rb +23 -0
- data/lib/hcheck/configuration.rb +57 -0
- data/lib/hcheck/configuration/service.rb +46 -0
- data/lib/hcheck/errors.rb +32 -0
- data/lib/hcheck/helper.rb +34 -0
- data/lib/hcheck/version.rb +5 -0
- metadata +316 -0
data/lib/hcheck.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
require 'hcheck/version'
|
8
|
+
require 'hcheck/helper'
|
9
|
+
require 'hcheck/application'
|
10
|
+
require 'hcheck/configuration'
|
11
|
+
require 'hcheck/errors'
|
12
|
+
|
13
|
+
# Main Hcheck module
|
14
|
+
module Hcheck
|
15
|
+
class << self
|
16
|
+
attr_accessor :configuration, :logging
|
17
|
+
|
18
|
+
LOG_FILE_PATH = 'log/hcheck.log'
|
19
|
+
|
20
|
+
def status
|
21
|
+
if configuration
|
22
|
+
configuration.services.map(&:check)
|
23
|
+
else
|
24
|
+
[{
|
25
|
+
name: 'Hcheck',
|
26
|
+
desc: 'Hcheck',
|
27
|
+
status: 'Hcheck configuration not found'
|
28
|
+
}]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure(config = {})
|
33
|
+
self.configuration ||= Configuration.new(config)
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
self.logging ||= set_logger
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def set_logger
|
43
|
+
dir = File.dirname(LOG_FILE_PATH)
|
44
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
45
|
+
logger = Logger.new(LOG_FILE_PATH, 'daily')
|
46
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
47
|
+
log_msg = "[#{severity}] [#{datetime}] #{msg}"
|
48
|
+
puts log_msg
|
49
|
+
"#{log_msg}\n"
|
50
|
+
end
|
51
|
+
logger
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'haml'
|
3
|
+
|
4
|
+
require 'hcheck'
|
5
|
+
require 'hcheck/application/helpers/responders'
|
6
|
+
|
7
|
+
module Hcheck
|
8
|
+
# base sinatra application
|
9
|
+
class SinatraBase < Sinatra::Base
|
10
|
+
set :public_dir, File.expand_path('application/assets', __dir__)
|
11
|
+
set :views, File.expand_path('application/views', __dir__)
|
12
|
+
set :haml, format: :html5
|
13
|
+
|
14
|
+
include Hcheck::ApplicationHelpers::Responders
|
15
|
+
end
|
16
|
+
|
17
|
+
# sinatra that gets booted when run in standalone mode
|
18
|
+
class Application < SinatraBase
|
19
|
+
get('/hcheck') { h_status }
|
20
|
+
end
|
21
|
+
|
22
|
+
# sinatra when mounted to rails
|
23
|
+
class Status < SinatraBase
|
24
|
+
def initialize(app = nil)
|
25
|
+
Hcheck::Configuration.load_default
|
26
|
+
rescue Hcheck::Errors::ConfigurationError => e
|
27
|
+
@config_error = e
|
28
|
+
ensure
|
29
|
+
super(app)
|
30
|
+
end
|
31
|
+
|
32
|
+
get('/') do
|
33
|
+
if @config_error
|
34
|
+
Hcheck.logger.error @config_error.message
|
35
|
+
|
36
|
+
respond_with Hcheck::Errors::ConfigurationError::MSG, 500, :error
|
37
|
+
else
|
38
|
+
h_status
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module ApplicationHelpers
|
3
|
+
# Sinatra app controller helpers
|
4
|
+
module Responders
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def h_status
|
8
|
+
authenticate!(params) if secured_access_enabled?
|
9
|
+
|
10
|
+
@status = Hcheck.status
|
11
|
+
|
12
|
+
if @status.find { |s| s[:status] == 'bad' }
|
13
|
+
status 503
|
14
|
+
else
|
15
|
+
status 200
|
16
|
+
end
|
17
|
+
|
18
|
+
haml :index
|
19
|
+
rescue Hcheck::Errors::InvalidAuthentication, Hcheck::Errors::IncompleteAuthSetup => e
|
20
|
+
status 401
|
21
|
+
@msg = e.message
|
22
|
+
|
23
|
+
haml :error
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_with(message, status_code, view)
|
27
|
+
status status_code
|
28
|
+
@msg = message
|
29
|
+
|
30
|
+
haml view
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def secured_access_enabled?
|
36
|
+
ENV['HCHECK_SECURE']
|
37
|
+
end
|
38
|
+
|
39
|
+
def authenticate!(params)
|
40
|
+
@token = params[:token]
|
41
|
+
access_precheck!
|
42
|
+
|
43
|
+
raise Hcheck::Errors::InvalidAuthentication unless ENV['HCHECK_ACCESS_TOKEN'].eql?(@token)
|
44
|
+
end
|
45
|
+
|
46
|
+
def access_precheck!
|
47
|
+
# throw error when hcheck secure is enabled but token is not set yet
|
48
|
+
raise Hcheck::Errors::IncompleteAuthSetup unless ENV['HCHECK_ACCESS_TOKEN'].present?
|
49
|
+
|
50
|
+
# throw error when token is not sent
|
51
|
+
raise Hcheck::Errors::InvalidAuthentication unless @token.present?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
%table.table
|
2
|
+
%tbody
|
3
|
+
%tr
|
4
|
+
%td
|
5
|
+
.error= @msg
|
6
|
+
:css
|
7
|
+
body {
|
8
|
+
background: #fefefe;
|
9
|
+
font-family: sans-serif;
|
10
|
+
}
|
11
|
+
.table {
|
12
|
+
width: 1024px;
|
13
|
+
margin: 0 auto;
|
14
|
+
}
|
15
|
+
.table td, .table th {
|
16
|
+
padding: 12px;
|
17
|
+
font-weight: 200;
|
18
|
+
text-align: center;
|
19
|
+
}
|
20
|
+
.table th {
|
21
|
+
font-size: 2em;
|
22
|
+
padding: 20px;
|
23
|
+
}
|
24
|
+
.error {
|
25
|
+
color: red
|
26
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
%table.table
|
2
|
+
%thead
|
3
|
+
%tr
|
4
|
+
%th{ colspan: 3 } Hcheck Status Page
|
5
|
+
%tbody
|
6
|
+
- @status.each do |s|
|
7
|
+
%tr
|
8
|
+
%td
|
9
|
+
#{s[:name]}
|
10
|
+
%td
|
11
|
+
#{s[:desc]}
|
12
|
+
%td
|
13
|
+
- case s[:status]
|
14
|
+
- when 'ok'
|
15
|
+
%span{style: 'color: green'} Ok
|
16
|
+
- when 'bad'
|
17
|
+
%span{style: 'color: red'} Bad
|
18
|
+
- else
|
19
|
+
%span{style: 'color: orange'}= s[:status]
|
20
|
+
|
21
|
+
:css
|
22
|
+
body {
|
23
|
+
background: #fefefe;
|
24
|
+
font-family: sans-serif;
|
25
|
+
}
|
26
|
+
.table {
|
27
|
+
width: 1024px;
|
28
|
+
margin: 0 auto;
|
29
|
+
}
|
30
|
+
.table td, .table th {
|
31
|
+
padding: 12px;
|
32
|
+
text-transform: capitalize;
|
33
|
+
font-weight: 200;
|
34
|
+
}
|
35
|
+
.table th {
|
36
|
+
font-size: 2em;
|
37
|
+
padding: 20px;
|
38
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# memcached check module
|
4
|
+
# implements status
|
5
|
+
# include memcached check dependencies
|
6
|
+
module Memcached
|
7
|
+
# @config { hosts, user, password }
|
8
|
+
def status(config)
|
9
|
+
client = Dalli::Client.new(config.delete(:url), config)
|
10
|
+
client.get('_')
|
11
|
+
'ok'
|
12
|
+
rescue Dalli::RingError => e
|
13
|
+
Hcheck.logger.error "[HCheck] Memcached::Error::NoServerAvailable #{e.message}"
|
14
|
+
'bad'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(_base)
|
18
|
+
require 'dalli'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# mongodb check module
|
4
|
+
# implements status
|
5
|
+
# include mongodb check dependencies
|
6
|
+
module Mongodb
|
7
|
+
# @config { hosts, user, password }
|
8
|
+
def status(config)
|
9
|
+
mongo_config = config.merge(connect_timeout: 3)
|
10
|
+
hosts = mongo_config.delete(:hosts).compact
|
11
|
+
client = Mongo::Client.new(hosts, mongo_config.merge(server_selection_timeout: hosts.count * 2))
|
12
|
+
client.database_names
|
13
|
+
client.close
|
14
|
+
'ok'
|
15
|
+
rescue Mongo::Error::NoServerAvailable => e
|
16
|
+
Hcheck.logger.error "[HCheck] Mongo::Error::NoServerAvailable #{e.message}"
|
17
|
+
'bad'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(_base)
|
21
|
+
require 'mongo'
|
22
|
+
Mongo::Logger.level = Logger::INFO
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# mysql check module
|
4
|
+
# implements status
|
5
|
+
# include mysql check dependencies
|
6
|
+
module Mysql
|
7
|
+
# @config { host, port, username, password, database }
|
8
|
+
def status(config)
|
9
|
+
connection = Mysql2::Client.new(config)
|
10
|
+
connection.close
|
11
|
+
'ok'
|
12
|
+
rescue Mysql2::Error => e
|
13
|
+
Hcheck.logger.error "[HCheck] Mysql2::Error::ConnectionError #{e.message}"
|
14
|
+
'bad'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(_base)
|
18
|
+
require 'mysql2'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# ping check module
|
4
|
+
# implements status
|
5
|
+
# include ping check dependencies
|
6
|
+
# returns ok only if the response code is 2** or 3**
|
7
|
+
module Ping
|
8
|
+
# @config { url }
|
9
|
+
def status(config)
|
10
|
+
url = URI.parse(config[:url])
|
11
|
+
request = build_request(url)
|
12
|
+
|
13
|
+
case request.request_head(url.path)
|
14
|
+
when Net::HTTPSuccess, Net::HTTPRedirection, Net::HTTPInformation
|
15
|
+
'ok'
|
16
|
+
else
|
17
|
+
'bad'
|
18
|
+
end
|
19
|
+
rescue Net::ReadTimeout, Errno::ECONNREFUSED => e
|
20
|
+
Hcheck.logger.error "[HCheck] Ping Fail #{e.message}"
|
21
|
+
'bad'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(_base)
|
25
|
+
require 'net/http'
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_request(url)
|
29
|
+
req = Net::HTTP.new(url.host, url.port)
|
30
|
+
req.use_ssl = true if url.scheme == 'https'
|
31
|
+
req.read_timeout = 5 # seconds
|
32
|
+
|
33
|
+
url.path = '/' if url.path.empty?
|
34
|
+
req
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# postgresql check module
|
4
|
+
# implements status
|
5
|
+
# include postgresql check dependencies
|
6
|
+
module Postgresql
|
7
|
+
# @config { host, port, options, tty, dbname, user, password }
|
8
|
+
def status(config)
|
9
|
+
config[:user] = config.delete(:username) if config[:username]
|
10
|
+
config[:dbname] = config.delete(:database) if config[:database]
|
11
|
+
|
12
|
+
PG::Connection.new(config).close
|
13
|
+
'ok'
|
14
|
+
rescue PG::ConnectionBad => e
|
15
|
+
Hcheck.logger.error "[HCheck] PG::ConnectionBad #{e.message}"
|
16
|
+
'bad'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(_base)
|
20
|
+
require 'pg'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# rabbitmq check module
|
4
|
+
# implements status
|
5
|
+
# include rabbitmq check dependencies
|
6
|
+
module Rabbitmq
|
7
|
+
# @config { host, vhost, port, user, pass }
|
8
|
+
def status(config)
|
9
|
+
connection = Bunny.new(config)
|
10
|
+
connection.start
|
11
|
+
connection.close
|
12
|
+
'ok'
|
13
|
+
rescue Bunny::TCPConnectionFailed,
|
14
|
+
Bunny::TCPConnectionFailedForAllHosts,
|
15
|
+
Bunny::NetworkFailure,
|
16
|
+
Bunny::AuthenticationFailureError,
|
17
|
+
Bunny::PossibleAuthenticationFailureError,
|
18
|
+
AMQ::Protocol::EmptyResponseError => e
|
19
|
+
|
20
|
+
Hcheck.logger.error "[HCheck] Bunny::Error #{e.message}"
|
21
|
+
'bad'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(_base)
|
25
|
+
require 'bunny'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hcheck
|
2
|
+
module Checks
|
3
|
+
# redis check module
|
4
|
+
# implements status
|
5
|
+
# include redis check dependencies
|
6
|
+
module Redis
|
7
|
+
# @config { host, port, db, password }
|
8
|
+
def status(config)
|
9
|
+
config[:sentinels] = config[:sentinels].map(&:symbolize_keys) if config[:sentinels]
|
10
|
+
|
11
|
+
::Redis.new(config).ping
|
12
|
+
'ok'
|
13
|
+
rescue ::Redis::CannotConnectError => e
|
14
|
+
Hcheck.logger.error "[HCheck] Redis server unavailable #{e.message}"
|
15
|
+
'bad'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(_base)
|
19
|
+
require 'redis'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
require 'hcheck/configuration/service'
|
6
|
+
|
7
|
+
module Hcheck
|
8
|
+
# configuration class that loads configs via various ways
|
9
|
+
# initializes service classes from config
|
10
|
+
class Configuration
|
11
|
+
attr_reader :services
|
12
|
+
DEFAULT_HCHECK_DIR = Gem.loaded_specs['rails'] ? 'config/' : ''
|
13
|
+
DEFAULT_CONFIG_PATH = [DEFAULT_HCHECK_DIR, 'hcheck.yml'].join
|
14
|
+
|
15
|
+
def initialize(config)
|
16
|
+
@services = config.map do |key, options|
|
17
|
+
options = [options] unless options.is_a?(Array)
|
18
|
+
options.map { |o| Service.new(key, o) }
|
19
|
+
end.flatten
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def load(config)
|
24
|
+
Hcheck.configure config
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_argv(args)
|
28
|
+
load_file(args[1].strip) if argv_config_present?(args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_file(path)
|
32
|
+
load read(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_default
|
36
|
+
load_file(DEFAULT_CONFIG_PATH)
|
37
|
+
end
|
38
|
+
|
39
|
+
def read(path)
|
40
|
+
YAML.safe_load(ERB.new(File.read(path)).result, [Symbol]) || {}
|
41
|
+
rescue StandardError => e
|
42
|
+
raise Hcheck::Errors::ConfigurationError, e
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_config
|
46
|
+
FileUtils.copy_file(Gem.loaded_specs['hcheck'].gem_dir + '/hcheck.sample.yml', DEFAULT_CONFIG_PATH)
|
47
|
+
puts "Generated #{DEFAULT_CONFIG_PATH}"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def argv_config_present?(argvs)
|
53
|
+
!argvs.empty? && argvs[0].match(/-+(config|c)/i)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|