rack-ecg 0.0.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class ECG
5
+ module Check
6
+ # @!method initialize
7
+ # Checks whether ActiveRecord is currently connected to the default
8
+ # database.
9
+ class ActiveRecordConnection
10
+ def result
11
+ value = ""
12
+ status = Status::OK
13
+ begin
14
+ if defined?(ActiveRecord)
15
+ value = ::ActiveRecord::Base.connection.active?
16
+ status = value ? Status::OK : Status::ERROR
17
+ else
18
+ status = Status::ERROR
19
+ value = "ActiveRecord not found"
20
+ end
21
+ rescue => e
22
+ status = Status::ERROR
23
+ value = e.message
24
+ end
25
+
26
+ Result.new(:active_record, status, value.to_s)
27
+ end
28
+
29
+ CheckRegistry.instance.register(:active_record, ActiveRecordConnection)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,16 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./static"
4
+
1
5
  module Rack
2
6
  class ECG
3
7
  module Check
4
- # if rack-ecg is serving a request - http is obviously working so far...
5
- # this is basically a "hello-world"
6
- class Error
7
- def result
8
- Result.new(:error, "error", "PC LOAD LETTER")
8
+ # @!method initialize
9
+ # Always returns a basic error for testing purposes.
10
+ class Error < Static
11
+ STATIC_PARAMETERS = {
12
+ name: :error,
13
+ success: false,
14
+ value: "PC LOAD LETTER",
15
+ }.freeze
16
+
17
+ def initialize
18
+ super(STATIC_PARAMETERS)
9
19
  end
10
20
  end
11
21
 
12
22
  CheckRegistry.instance.register(:error, Error)
13
-
14
23
  end
15
24
  end
16
25
  end
@@ -1,13 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ECG
3
5
  module Check
6
+ # @deprecated This check requires the presence of the git executable, and executes it every time to determine the
7
+ # current revision. Consider checking the revision at initialization time, and returning it via a {Static} check
8
+ # instead.
9
+ #
10
+ # @!method initialize
11
+ # Returns the SHA1 of the current commit, as reported by the git
12
+ # executable.
4
13
  class GitRevision
5
14
  def result
6
15
  _stdin, stdout, stderr, wait_thread = Open3.popen3("git rev-parse HEAD")
7
16
 
8
17
  success = wait_thread.value.success?
9
18
 
10
- status = success ? "ok" : "error"
19
+ status = success ? Status::OK : Status::ERROR
11
20
 
12
21
  value = success ? stdout.read : stderr.read
13
22
  value = value.strip
@@ -17,7 +26,6 @@ module Rack
17
26
  end
18
27
 
19
28
  CheckRegistry.instance.register(:git_revision, GitRevision)
20
-
21
29
  end
22
30
  end
23
31
  end
@@ -1,16 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./static"
4
+
1
5
  module Rack
2
6
  class ECG
3
7
  module Check
4
- # if rack-ecg is serving a request - http is obviously working so far...
5
- # this is basically a "hello-world"
6
- class Http
7
- def result
8
- Result.new(:http, "ok", "online")
8
+ # @!method initialize
9
+ # Always returns a success.
10
+ class Http < Static
11
+ STATIC_PARAMETERS = {
12
+ name: :http,
13
+ success: true,
14
+ value: "online",
15
+ }.freeze
16
+
17
+ def initialize
18
+ super(STATIC_PARAMETERS)
9
19
  end
10
20
  end
11
21
 
12
22
  CheckRegistry.instance.register(:http, Http)
13
-
14
23
  end
15
24
  end
16
25
  end
@@ -1,20 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ECG
3
5
  module Check
6
+ # @!method initialize
7
+ # Returns the latest applied ActiveRecord migration in the default
8
+ # database.
4
9
  class MigrationVersion
5
10
  def result
6
11
  value = ""
7
- status = "ok"
12
+ status = Status::OK
8
13
  begin
9
14
  if defined?(ActiveRecord)
10
15
  connection = ActiveRecord::Base.connection
11
16
  value = connection.select_value("select max(version) from schema_migrations")
12
17
  else
13
- status = "error"
18
+ status = Status::ERROR
14
19
  value = "ActiveRecord not found"
15
20
  end
16
21
  rescue => e
17
- status = "error"
22
+ status = Status::ERROR
18
23
  value = e.message
19
24
  end
20
25
 
@@ -23,7 +28,6 @@ module Rack
23
28
  end
24
29
 
25
30
  CheckRegistry.instance.register(:migration_version, MigrationVersion)
26
-
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class ECG
5
+ module Check
6
+ # @!method initialize
7
+ # Checks whether the given Redis client is currently connected to the
8
+ # database as identified by the ++instance++ option.
9
+ #
10
+ # @option parameters instance [Redis] The Redis client
11
+ class RedisConnection
12
+ attr_reader :redis_instance
13
+
14
+ def initialize(parameters = {})
15
+ @redis_instance = parameters[:instance]
16
+ end
17
+
18
+ def result
19
+ value = ""
20
+ status = Status::OK
21
+ begin
22
+ if redis_instance.nil?
23
+ status = Status::ERROR
24
+ value = "Redis instance parameters not found"
25
+ elsif defined?(::Redis)
26
+ value = redis_instance.connected?
27
+ status = value ? Status::OK : Status::ERROR
28
+ else
29
+ status = Status::ERROR
30
+ value = "Redis not found"
31
+ end
32
+ rescue => e
33
+ status = Status::ERROR
34
+ value = e.message
35
+ end
36
+
37
+ Result.new(:redis, status, value.to_s)
38
+ end
39
+
40
+ CheckRegistry.instance.register(:redis, RedisConnection)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class ECG
5
+ module Check
6
+ class SequelConnection
7
+ attr_reader :connection_parameters, :name
8
+
9
+ # Checks whether Sequel can connect to the database identified by the
10
+ # ++connection++ option.
11
+ #
12
+ # @option parameters connection [String,Hash] Sequel connection parameters to check
13
+ # @option parameters name [String,nil] Name to distinguish multiple Sequel checks
14
+ def initialize(parameters = {})
15
+ @connection_parameters = parameters[:connection]
16
+ @name = parameters[:name]
17
+ end
18
+
19
+ def result
20
+ value = ""
21
+ status = Status::OK
22
+ begin
23
+ if connection_parameters.nil?
24
+ status = Status::ERROR
25
+ value = "Sequel Connection parameters not found"
26
+ elsif defined?(::Sequel)
27
+ ::Sequel.connect(connection_parameters) do |db|
28
+ value = db.test_connection
29
+ status = Status::OK
30
+ end
31
+ else
32
+ status = Status::ERROR
33
+ value = "Sequel not found"
34
+ end
35
+ rescue => e
36
+ status = Status::ERROR
37
+ value = e.message
38
+ end
39
+
40
+ Result.new(result_key.to_sym, status, value.to_s)
41
+ end
42
+
43
+ def result_key
44
+ if name
45
+ "sequel #{name.downcase}".gsub(/\W+/, "_")
46
+ else
47
+ "sequel"
48
+ end
49
+ end
50
+
51
+ CheckRegistry.instance.register(:sequel, SequelConnection)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class ECG
5
+ module Check
6
+ class Static
7
+ # Always returns the provided ++value++ under the ++name++ key, with the result set by ++status++.
8
+ #
9
+ # @example Return "Hello, world!" under ++static++
10
+ # use(Rack::ECG, { checks: [[:static, { value: "Hello, world!" }]] })
11
+ #
12
+ # @example Return "Paper jam in tray 2" as an error under ++printer_status++
13
+ # use(Rack::ECG, {
14
+ # checks: [
15
+ # [
16
+ # :static,
17
+ # {
18
+ # value: "Paper jam in tray 2",
19
+ # success: false, # or status: Rack::ECG::Check::Status::ERROR
20
+ # name: :printer_status,
21
+ # },
22
+ # ],
23
+ # ],
24
+ # })
25
+ #
26
+ # @option parameters value [Object] (nil) Result value
27
+ # @option parameters status [Status::ERROR, Status::OK, nil] (nil) Result status (takes precedence over
28
+ # ++success++)
29
+ # @option parameters success [Boolean] (true) Whether the result is successful
30
+ # @option parameters name [Symbol, #to_sym] (:static) Key for the check result in the response
31
+ def initialize(parameters)
32
+ parameters ||= {}
33
+
34
+ @name = parameters.fetch(:name, :static).to_sym
35
+ @value = parameters.fetch(:value, nil)
36
+
37
+ @status = if parameters.key?(:status)
38
+ parameters[:status]
39
+ else
40
+ parameters.fetch(:success, true) ? Status::OK : Status::ERROR
41
+ end
42
+ end
43
+
44
+ def result
45
+ Result.new(@name, @status, @value)
46
+ end
47
+ end
48
+
49
+ CheckRegistry.instance.register(:static, Static)
50
+ end
51
+ end
52
+ end
@@ -1,15 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack/ecg/check_registry"
4
+ require "rack/ecg/check/active_record_connection"
2
5
  require "rack/ecg/check/error"
3
6
  require "rack/ecg/check/git_revision"
4
7
  require "rack/ecg/check/http"
5
8
  require "rack/ecg/check/migration_version"
9
+ require "rack/ecg/check/redis_connection"
10
+ require "rack/ecg/check/sequel_connection"
11
+ require "rack/ecg/check/static"
6
12
 
7
13
  module Rack
8
14
  class ECG
9
15
  module Check
16
+ # Possible recognised check statuses.
17
+ module Status
18
+ # Indicates the check was successful.
19
+ OK = "ok"
20
+ # Indicates the check errored.
21
+ ERROR = "error"
22
+ end
23
+
10
24
  class Result < Struct.new(:name, :status, :value)
25
+ # Format the result as a JSON compatible hash.
26
+ #
27
+ # @return [Hash<Object, Hash<Symbol, Object>>] Result in a hash format.
28
+ # @example A HTTP success response
29
+ # puts result.as_json
30
+ # # {:http=>{:status=>"ok", :value=>"online"}}
31
+ def as_json
32
+ { name => { status: status, value: value } }
33
+ end
34
+
35
+ # Return the result as a JSON object.
36
+ #
37
+ # @return [String] Result in a JSON object string.
38
+ # @example A HTTP success response
39
+ # puts result.to_json
40
+ # # {"http": {"status": "ok", "value": "online"}}
11
41
  def to_json
12
- {name => {:status => status, :value => value}}
42
+ JSON.dump(as_json)
13
43
  end
14
44
  end
15
45
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/ecg/check"
4
+
5
+ module Rack
6
+ class ECG
7
+ class CheckFactory
8
+ CheckDefinition = Struct.new(:check_class, :parameters)
9
+
10
+ def initialize(definitions, default_checks = [])
11
+ definitions = Array(definitions) | default_checks
12
+
13
+ @checks = definitions.map do |check_name, check_parameters|
14
+ CheckDefinition.new(CheckRegistry.lookup(check_name), check_parameters)
15
+ end
16
+ end
17
+
18
+ def build_all
19
+ @checks.map do |check_definition|
20
+ build(check_definition.check_class, check_definition.parameters)
21
+ end
22
+ end
23
+
24
+ def build(check_class, parameters = nil)
25
+ parameters.nil? ? check_class.new : check_class.new(parameters)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,20 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "singleton"
2
4
 
3
5
  module Rack
4
6
  class ECG
5
7
  class CheckRegistry
8
+ # Raised when a check didn't exist during lookup
9
+ CheckNotRegistered = Class.new(StandardError)
6
10
  include Singleton
7
11
 
8
- def initialize()
12
+ # Constructs the singleton instance of the registry
13
+ def initialize
9
14
  @registry = {}
10
15
  end
11
16
 
17
+ # Register a check class by name
18
+ #
19
+ # @param [Symbol] name Desired check name
20
+ # @param [Class] check_class Class implementing check functionality
12
21
  def register(name, check_class)
13
22
  @registry[name] = check_class
14
23
  end
15
24
 
16
- def [](name)
17
- @registry[name]
25
+ # Fetches the registered check class by name
26
+ #
27
+ # @param [Symbol] name Registered check name
28
+ # @raise [CheckNotRegistered] if the named check has not been registered
29
+ def lookup(name)
30
+ @registry.fetch(name) { raise CheckNotRegistered, "Check '#{name}' is not registered" }
31
+ end
32
+
33
+ # (see #lookup)
34
+ def self.lookup(name)
35
+ instance.lookup(name)
36
+ end
37
+
38
+ # (see #register)
39
+ def self.register(name, check_class)
40
+ instance.register(name, check_class)
18
41
  end
19
42
  end
20
43
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ECG
3
- VERSION = "0.0.3"
5
+ # Library version.
6
+ VERSION = "0.2.0"
4
7
  end
5
8
  end
data/lib/rack/ecg.rb CHANGED
@@ -1,41 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack/ecg/version"
2
4
  require "json"
3
5
  require "open3"
4
- require "rack/ecg/check"
6
+ require "rack/ecg/check_factory"
5
7
 
6
8
  module Rack
7
9
  class ECG
10
+ # Default mount path.
8
11
  DEFAULT_MOUNT_AT = "/_ecg"
9
- DEFAULT_CHECKS = [ :http ]
10
-
11
- def initialize(app=nil, options={})
12
+ # Checks enabled by default.
13
+ DEFAULT_CHECKS = [:http]
14
+
15
+ # Constructs an instance of ECG Rack middleware with the specified
16
+ # options.
17
+ #
18
+ # @param app [Object,nil] Underlying Rack application to receive unmatched
19
+ # requests. If unset, any unmatched requests will return a 404.
20
+ # @param checks [Array<Symbol, Array<Symbol, Object>>] Sets and
21
+ # configures the checks run by this instance.
22
+ # @param at [String, nil] Path which this ECG instance handles.
23
+ # @param hook [#call, nil] Callable which receives the success status and
24
+ # check results
25
+ def initialize(app = nil, checks: DEFAULT_CHECKS, at: DEFAULT_MOUNT_AT, hook: nil)
12
26
  @app = app
13
27
 
14
- check_names = options.delete(:checks) || []
15
- @check_classes = build_check_classes(check_names)
28
+ check_configuration = checks || []
29
+ @check_factory = CheckFactory.new(check_configuration, DEFAULT_CHECKS)
30
+ @mount_at = at || DEFAULT_MOUNT_AT
16
31
 
17
- @at = options.delete(:at) || DEFAULT_MOUNT_AT
18
-
19
- @hook = options.delete(:hook)
32
+ @result_hook = hook
20
33
  end
21
34
 
35
+ # Rack compatible call method. Not intended for direct usage.
22
36
  def call(env)
23
- if env["PATH_INFO"] == @at
37
+ if env["PATH_INFO"] == @mount_at
38
+ check_results = @check_factory.build_all.inject({}) do |results, check|
39
+ results.merge(check.result.as_json)
40
+ end
24
41
 
25
- check_results = @check_classes.inject({}){|results, check_class|
26
- check = check_class.new
27
- results.merge(check.result.to_json)
28
- }
29
-
30
- success = check_results.none? { |check| check[1][:status] == "error" }
42
+ success = check_results.none? { |check| check[1][:status] == Check::Status::ERROR }
31
43
 
32
44
  response_status = success ? 200 : 500
33
45
 
34
- @hook.call(success, check_results) if @hook
46
+ @result_hook&.call(success, check_results)
35
47
 
36
48
  response_headers = {
37
- "X-Rack-ECG-Version" => Rack::ECG::VERSION,
38
- "Content-Type" => "application/json"
49
+ "X-Rack-ECG-Version" => Rack::ECG::VERSION,
50
+ "Content-Type" => "application/json",
39
51
  }
40
52
 
41
53
  response_body = JSON.pretty_generate(check_results)
@@ -44,19 +56,8 @@ module Rack
44
56
  elsif @app
45
57
  @app.call(env)
46
58
  else
47
- [404, {},[]]
59
+ [404, {}, []]
48
60
  end
49
61
  end
50
-
51
- private
52
- def build_check_classes(check_names)
53
- check_names = Array(check_names) # handle nil, or not a list
54
- check_names = check_names | DEFAULT_CHECKS # add the :http check if it's not there
55
- check_names.map{|check_name|
56
- check_class = CheckRegistry.instance[check_name]
57
- raise "Don't know about check #{check_name}" unless check_class
58
- check_class
59
- }
60
- end
61
62
  end
62
63
  end
data/lib/rack-ecg.rb CHANGED
@@ -1 +1,3 @@
1
- require 'rack/ecg'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/ecg"
data/rack-ecg.gemspec CHANGED
@@ -1,28 +1,47 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'rack/ecg/version'
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "lib/rack/ecg/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "rack-ecg"
8
8
  spec.version = Rack::ECG::VERSION
9
9
  spec.authors = ["Envato", "Julian Doherty"]
10
10
  spec.email = ["julian@envato.com"]
11
- spec.summary = %q{Rack middleware serving a health check page}
12
- spec.description = %q{rack-ecg allows you to serve a page that shows you facts about your deployed app to allow you to check that everything is running as it should: git revision, database migrations, and more}
11
+ spec.summary = "Rack middleware serving a health check page"
12
+ spec.description = <<-EOF
13
+ rack-ecg allows you to serve a page that shows you facts about your deployed
14
+ app to allow you to check that everything is running as it should: git
15
+ revision, database migrations, and more
16
+ EOF
13
17
  spec.homepage = "https://github.com/envato/rack-ecg"
14
18
  spec.license = "MIT"
15
19
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = "https://github.com/envato/rack-ecg/blob/main/CHANGELOG.md"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
19
31
  spec.require_paths = ["lib"]
20
32
 
21
- spec.add_runtime_dependency "rack"
33
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
34
+
35
+ spec.add_runtime_dependency("rack")
22
36
 
23
- spec.add_development_dependency "bundler", "~> 1.7"
24
- spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec", "~> 3.2.0"
26
- spec.add_development_dependency "rack-test", "~> 0.6.3"
27
- spec.add_development_dependency "pry", "~> 0.10.1"
37
+ spec.add_development_dependency("bundler", "~> 2.3.7")
38
+ spec.add_development_dependency("pry", "~> 0.14.1")
39
+ spec.add_development_dependency("rack-test", "~> 1.1.0")
40
+ spec.add_development_dependency("rake", "~> 13.0")
41
+ spec.add_development_dependency("redcarpet", "~> 3.5.0")
42
+ spec.add_development_dependency("rspec", "~> 3.11.0")
43
+ spec.add_development_dependency("rubocop-rake", "> 0")
44
+ spec.add_development_dependency("rubocop-rspec", "> 0")
45
+ spec.add_development_dependency("rubocop-shopify", "~> 2.4.0")
46
+ spec.add_development_dependency("yard", "~> 0.9.24")
28
47
  end