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.
- checksums.yaml +5 -5
- data/.github/workflows/main.yml +22 -0
- data/.rubocop.yml +21 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +69 -0
- data/Gemfile +1 -0
- data/README.md +118 -31
- data/Rakefile +10 -1
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/examples/basic.ru +3 -3
- data/examples/checks.ru +3 -3
- data/examples/hook.ru +17 -0
- data/examples/mounted_path.ru +3 -3
- data/examples/parameters.ru +12 -0
- data/examples/stand_alone.ru +4 -0
- data/lib/rack-ecg.rb +1 -0
- data/lib/rack/ecg.rb +37 -55
- data/lib/rack/ecg/check.rb +45 -0
- data/lib/rack/ecg/check/active_record_connection.rb +32 -0
- data/lib/rack/ecg/check/error.rb +16 -0
- data/lib/rack/ecg/check/git_revision.rb +26 -0
- data/lib/rack/ecg/check/http.rb +16 -0
- data/lib/rack/ecg/check/migration_version.rb +32 -0
- data/lib/rack/ecg/check/redis_connection.rb +34 -0
- data/lib/rack/ecg/check/sequel_connection.rb +54 -0
- data/lib/rack/ecg/check_factory.rb +28 -0
- data/lib/rack/ecg/check_registry.rb +43 -0
- data/lib/rack/ecg/version.rb +3 -1
- data/rack-ecg.gemspec +31 -14
- metadata +91 -31
- data/.travis.yml +0 -15
- data/spec/rack_middleware_spec.rb +0 -146
- data/spec/spec_helper.rb +0 -23
data/examples/mounted_path.ru
CHANGED
@@ -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"]] })
|
data/lib/rack-ecg.rb
CHANGED
data/lib/rack/ecg.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
@
|
15
|
-
@
|
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"] == @
|
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
|
-
|
43
|
+
response_status = success ? 200 : 500
|
22
44
|
|
23
|
-
|
45
|
+
@result_hook&.call(success, check_results)
|
24
46
|
|
25
47
|
response_headers = {
|
26
|
-
"X-Rack-ECG-Version"
|
27
|
-
"Content-Type"
|
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
|
-
|
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
|