hcheck 0.1.0

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.
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