rack-ecg 0.0.1 → 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.
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'rack/ecg'
2
3
 
3
- use Rack::ECG, at: "/health_check"
4
- use Rack::Reloader
4
+ use(Rack::ECG, at: "/health_check")
5
5
 
6
- run -> (env) { [200, {}, ["Hello, World"]] }
6
+ run(-> (_env) { [200, {}, ["Hello, World"]] })
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'rack/ecg'
3
+ require 'sequel'
4
+ require 'sqlite3'
5
+
6
+ use(Rack::ECG, checks: [
7
+ :http,
8
+ [:sequel, { connection: 'sqlite://events.db', name: 'events' }],
9
+ [:sequel, { connection: 'sqlite://projections.db', name: 'projections' }],
10
+ ])
11
+
12
+ run(-> (_env) { [200, {}, ["Hello, World"]] })
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require 'rack/ecg'
3
+
4
+ run(Rack::ECG.new)
@@ -1 +1,2 @@
1
+ # frozen_string_literal: true
1
2
  require 'rack/ecg'
@@ -1,80 +1,62 @@
1
+ # frozen_string_literal: true
1
2
  require "rack/ecg/version"
2
3
  require "json"
3
4
  require "open3"
5
+ require "rack/ecg/check_factory"
4
6
 
5
7
  module Rack
6
8
  class ECG
9
+ # Default mount path.
7
10
  DEFAULT_MOUNT_AT = "/_ecg"
8
- DEFAULT_CHECKS = [ :check_http ]
9
-
10
- def initialize(app, options={})
11
+ # Checks enabled by default.
12
+ DEFAULT_CHECKS = [:http]
13
+
14
+ # Constructs an instance of ECG Rack middleware with the specified
15
+ # options.
16
+ #
17
+ # @param app [Object,nil] Underlying Rack application to receive unmatched
18
+ # requests. If unset, any unmatched requests will return a 404.
19
+ # @param checks [Array<Symbol, Array<Symbol, Object>>] Sets and
20
+ # configures the checks run by this instance.
21
+ # @param at [String, nil] Path which this ECG instance handles.
22
+ # @param hook [#call, nil] Callable which receives the success status and
23
+ # check results
24
+ def initialize(app = nil, checks: DEFAULT_CHECKS, at: DEFAULT_MOUNT_AT, hook: nil)
11
25
  @app = app
12
- option_checks = options.delete(:checks) || []
13
- option_checks = option_checks.map{|check| "check_#{check}".to_sym }
14
- @checks = DEFAULT_CHECKS + option_checks
15
- @at = options.delete(:at) || DEFAULT_MOUNT_AT
26
+
27
+ check_configuration = checks || []
28
+ @check_factory = CheckFactory.new(check_configuration, DEFAULT_CHECKS)
29
+ @mount_at = at || DEFAULT_MOUNT_AT
30
+
31
+ @result_hook = hook
16
32
  end
17
33
 
34
+ # Rack compatible call method. Not intended for direct usage.
18
35
  def call(env)
19
- if env["PATH_INFO"] == @at
36
+ if env["PATH_INFO"] == @mount_at
37
+ check_results = @check_factory.build_all.inject({}) do |results, check|
38
+ results.merge(check.result.as_json)
39
+ end
40
+
41
+ success = check_results.none? { |check| check[1][:status] == Check::Status::ERROR }
20
42
 
21
- check_results = @checks.inject({}){|results, check_method| results.merge(send(check_method)) }
43
+ response_status = success ? 200 : 500
22
44
 
23
- response_status = check_results.any?{|check| check[1][:status] == "error" } ? 500 : 200
45
+ @result_hook&.call(success, check_results)
24
46
 
25
47
  response_headers = {
26
- "X-Rack-ECG-Version" => Rack::ECG::VERSION,
27
- "Content-Type" => "application/json"
48
+ "X-Rack-ECG-Version" => Rack::ECG::VERSION,
49
+ "Content-Type" => "application/json",
28
50
  }
29
51
 
30
52
  response_body = JSON.pretty_generate(check_results)
31
53
 
32
54
  [response_status, response_headers, [response_body]]
33
- else
55
+ elsif @app
34
56
  @app.call(env)
57
+ else
58
+ [404, {}, []]
35
59
  end
36
60
  end
37
-
38
- private
39
- def check_http
40
- # if rack-ecg is serving a request - http is obviously working so far...
41
- # this is basically a "hello-world"
42
- {http: {status: "ok", value: "online" } }
43
- end
44
-
45
- def check_error
46
- # this always fails. mainly for testing
47
- {error: {status: "error", value: "PC LOAD LETTER" } }
48
- end
49
-
50
- def check_git_revision
51
- _stdin, stdout, stderr, wait_thread = Open3.popen3("git rev-parse HEAD")
52
-
53
- success = wait_thread.value.success?
54
- status = success ? "ok" : "error"
55
- value = success ? stdout.read : stderr.read
56
- value = value.strip
57
- {git_revision: {status: status, value: value} }
58
- end
59
-
60
- def check_migration_version
61
- value = ""
62
- status = "ok"
63
- begin
64
- if defined?(ActiveRecord)
65
- connection = ActiveRecord::Base.connection
66
- result_set = connection.execute("select max(version) as version from schema_migrations")
67
- version = result_set.first
68
- value = version["version"]
69
- else
70
- status = "error"
71
- value = "ActiveRecord not found"
72
- end
73
- rescue => e
74
- status = "error"
75
- value = e.message
76
- end
77
- {migration_version: {status: status, value: value} }
78
- end
79
61
  end
80
62
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require "rack/ecg/check_registry"
3
+ require "rack/ecg/check/error"
4
+ require "rack/ecg/check/git_revision"
5
+ require "rack/ecg/check/http"
6
+ require "rack/ecg/check/migration_version"
7
+ require "rack/ecg/check/active_record_connection"
8
+ require "rack/ecg/check/redis_connection"
9
+ require "rack/ecg/check/sequel_connection"
10
+
11
+ module Rack
12
+ class ECG
13
+ module Check
14
+ # Possible recognised check statuses.
15
+ module Status
16
+ # Indicates the check was successful.
17
+ OK = "ok"
18
+ # Indicates the check errored.
19
+ ERROR = "error"
20
+ end
21
+
22
+ class Result < Struct.new(:name, :status, :value)
23
+ # Format the result as a JSON compatible hash.
24
+ #
25
+ # @return [Hash<Object, Hash<Symbol, Object>>] Result in a hash format.
26
+ # @example A HTTP success response
27
+ # puts result.as_json
28
+ # # {:http=>{:status=>"ok", :value=>"online"}}
29
+ def as_json
30
+ { name => { status: status, value: value } }
31
+ end
32
+
33
+ # Return the result as a JSON object.
34
+ #
35
+ # @return [String] Result in a JSON object string.
36
+ # @example A HTTP success response
37
+ # puts result.to_json
38
+ # # {"http": {"status": "ok", "value": "online"}}
39
+ def to_json
40
+ JSON.dump(as_json)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Checks whether ActiveRecord is currently connected to the default
7
+ # database.
8
+ class ActiveRecordConnection
9
+ def result
10
+ value = ""
11
+ status = Status::OK
12
+ begin
13
+ if defined?(ActiveRecord)
14
+ value = ::ActiveRecord::Base.connection.active?
15
+ status = value ? Status::OK : Status::ERROR
16
+ else
17
+ status = Status::ERROR
18
+ value = "ActiveRecord not found"
19
+ end
20
+ rescue => e
21
+ status = Status::ERROR
22
+ value = e.message
23
+ end
24
+
25
+ Result.new(:active_record, status, value.to_s)
26
+ end
27
+
28
+ CheckRegistry.instance.register(:active_record, ActiveRecordConnection)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Always returns a basic error for testing purposes.
7
+ class Error
8
+ def result
9
+ Result.new(:error, Status::ERROR, "PC LOAD LETTER")
10
+ end
11
+ end
12
+
13
+ CheckRegistry.instance.register(:error, Error)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Returns the SHA1 of the current commit, as reported by the git
7
+ # executable.
8
+ class GitRevision
9
+ def result
10
+ _stdin, stdout, stderr, wait_thread = Open3.popen3("git rev-parse HEAD")
11
+
12
+ success = wait_thread.value.success?
13
+
14
+ status = success ? Status::OK : Status::ERROR
15
+
16
+ value = success ? stdout.read : stderr.read
17
+ value = value.strip
18
+
19
+ Result.new(:git_revision, status, value)
20
+ end
21
+ end
22
+
23
+ CheckRegistry.instance.register(:git_revision, GitRevision)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Always returns a success.
7
+ class Http
8
+ def result
9
+ Result.new(:http, Status::OK, "online")
10
+ end
11
+ end
12
+
13
+ CheckRegistry.instance.register(:http, Http)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Returns the latest applied ActiveRecord migration in the default
7
+ # database.
8
+ class MigrationVersion
9
+ def result
10
+ value = ""
11
+ status = Status::OK
12
+ begin
13
+ if defined?(ActiveRecord)
14
+ connection = ActiveRecord::Base.connection
15
+ value = connection.select_value("select max(version) from schema_migrations")
16
+ else
17
+ status = Status::ERROR
18
+ value = "ActiveRecord not found"
19
+ end
20
+ rescue => e
21
+ status = Status::ERROR
22
+ value = e.message
23
+ end
24
+
25
+ Result.new(:migration_version, status, value)
26
+ end
27
+ end
28
+
29
+ CheckRegistry.instance.register(:migration_version, MigrationVersion)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ # @!method initialize
6
+ # Checks whether the global Redis client is currently connected to the
7
+ # database.
8
+ #
9
+ # Does not take any options.
10
+ class RedisConnection
11
+ def result
12
+ value = ""
13
+ status = Status::OK
14
+ begin
15
+ if defined?(::Redis)
16
+ value = ::Redis.current.connected?
17
+ status = value ? Status::OK : Status::ERROR
18
+ else
19
+ status = Status::ERROR
20
+ value = "Redis not found"
21
+ end
22
+ rescue => e
23
+ status = Status::ERROR
24
+ value = e.message
25
+ end
26
+
27
+ Result.new(:redis, status, value.to_s)
28
+ end
29
+
30
+ CheckRegistry.instance.register(:redis, RedisConnection)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class ECG
4
+ module Check
5
+ class SequelConnection
6
+ attr_reader :connection_parameters, :name
7
+
8
+ # Checks whether Sequel can connect to the database identified by the
9
+ # ++connection++ option.
10
+ #
11
+ # @option parameters connection [String,Hash] Sequel connection parameters to check
12
+ # @option parameters name [String,nil] Name to distinguish multiple Sequel checks
13
+ def initialize(parameters = {})
14
+ @connection_parameters = parameters[:connection]
15
+ @name = parameters[:name]
16
+ end
17
+
18
+ def result
19
+ value = ""
20
+ status = Status::OK
21
+ begin
22
+ if connection_parameters.nil?
23
+ status = Status::ERROR
24
+ value = "Sequel Connection parameters not found"
25
+ elsif defined?(::Sequel)
26
+ ::Sequel.connect(connection_parameters) do |db|
27
+ value = db.test_connection
28
+ status = Status::OK
29
+ end
30
+ else
31
+ status = Status::ERROR
32
+ value = "Sequel not found"
33
+ end
34
+ rescue => e
35
+ status = Status::ERROR
36
+ value = e.message
37
+ end
38
+
39
+ Result.new(result_key.to_sym, status, value.to_s)
40
+ end
41
+
42
+ def result_key
43
+ if name
44
+ "sequel #{name.downcase}".gsub(/\W+/, '_')
45
+ else
46
+ "sequel"
47
+ end
48
+ end
49
+
50
+ CheckRegistry.instance.register(:sequel, SequelConnection)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require "rack/ecg/check"
3
+
4
+ module Rack
5
+ class ECG
6
+ class CheckFactory
7
+ CheckDefinition = Struct.new(:check_class, :parameters)
8
+
9
+ def initialize(definitions, default_checks = [])
10
+ definitions = Array(definitions) | default_checks
11
+
12
+ @checks = definitions.map do |check_name, check_parameters|
13
+ CheckDefinition.new(CheckRegistry.lookup(check_name), check_parameters)
14
+ end
15
+ end
16
+
17
+ def build_all
18
+ @checks.map do |check_definition|
19
+ build(check_definition.check_class, check_definition.parameters)
20
+ end
21
+ end
22
+
23
+ def build(check_class, parameters = nil)
24
+ parameters.nil? ? check_class.new : check_class.new(parameters)
25
+ end
26
+ end
27
+ end
28
+ end