rack-ecg 0.0.3 → 0.2.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.
@@ -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