rack-ecg 0.0.1 → 0.1.0

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