hcheck 0.1.0

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