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.
- 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
|