rails_health_checks 1.0.1 → 1.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 +4 -4
- data/README.md +107 -1
- data/lib/rails_health_checks/check_registry.rb +17 -5
- data/lib/rails_health_checks/configuration.rb +2 -1
- data/lib/rails_health_checks/rack/app.rb +143 -0
- data/lib/rails_health_checks/version.rb +1 -1
- data/lib/rails_health_checks.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 934bfd1962ca10009ba6925f81cbf57f09d8d63ab017a6452296bfb32994352b
|
|
4
|
+
data.tar.gz: 7ba0cfd783e433b26de67d2f9a2dc00a13e341584c88b1559c5f08e1f237187c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e52cb1373a3a8c26bd7a01ceefa6a32dedfb5c06421d754d94150a56349349b3ef467e529e696e9a74efd2449b7f8078bfff5b1a3cf9f2915786e8b16468d87
|
|
7
|
+
data.tar.gz: 9c1dbc7ea623885abc7cf39acbdc63741fffb25698c93d3beddc501f598783956ede451c60456b8ba1e54bc54c1b725fb70a2a2edc727e6836081d8ff93587fc
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ A Rails engine that adds production-grade health check endpoints to any Rails ap
|
|
|
22
22
|
## Table of Contents
|
|
23
23
|
|
|
24
24
|
- [Installation](#installation)
|
|
25
|
+
- [Rack Applications](#rack-applications)
|
|
25
26
|
- [Endpoints](#endpoints)
|
|
26
27
|
- [Configuration](#configuration)
|
|
27
28
|
- [Configuration Reference](#configuration-reference)
|
|
@@ -66,6 +67,111 @@ mount RailsHealthChecks::Engine => "/health"
|
|
|
66
67
|
|
|
67
68
|
---
|
|
68
69
|
|
|
70
|
+
## Rack Applications
|
|
71
|
+
|
|
72
|
+
`RailsHealthChecks::Rack::App` is a mountable Rack app that exposes the same endpoints without requiring ActionDispatch or Rails routing. It is opt-in — the Rails engine is unaffected.
|
|
73
|
+
|
|
74
|
+
### Setup
|
|
75
|
+
|
|
76
|
+
Add to your Gemfile (the gem already lists `rails >= 8.0` as a dependency, so `activesupport` and `concurrent-ruby` are available):
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
gem "rails_health_checks"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Require and mount the Rack app alongside your existing app:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# config.ru
|
|
86
|
+
require "rails_health_checks"
|
|
87
|
+
require "rails_health_checks/rack/app"
|
|
88
|
+
|
|
89
|
+
RailsHealthChecks.configure do |config|
|
|
90
|
+
config.checks = [:disk, :memory, :redis]
|
|
91
|
+
config.redis_url = ENV["REDIS_URL"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
map "/health" do
|
|
95
|
+
run RailsHealthChecks::Rack::App
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
run MyApp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Sinatra
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
require "rails_health_checks/rack/app"
|
|
105
|
+
|
|
106
|
+
class MyApp < Sinatra::Base
|
|
107
|
+
use Rack::URLMap, "/health" => RailsHealthChecks::Rack::App
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Roda
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
require "rails_health_checks/rack/app"
|
|
115
|
+
|
|
116
|
+
class MyApp < Roda
|
|
117
|
+
plugin :multi_run
|
|
118
|
+
run "/health", RailsHealthChecks::Rack::App
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Available endpoints
|
|
123
|
+
|
|
124
|
+
The routes are identical to the Rails engine, relative to the mount point:
|
|
125
|
+
|
|
126
|
+
| Endpoint | Format | Use case |
|
|
127
|
+
|----------|--------|----------|
|
|
128
|
+
| `GET/HEAD /` | JSON | Health status |
|
|
129
|
+
| `GET/HEAD /live` | Plain text | Liveness probe |
|
|
130
|
+
| `GET /metrics` | Prometheus text | Prometheus scraping |
|
|
131
|
+
| `GET /:group` | JSON | Scoped check group |
|
|
132
|
+
|
|
133
|
+
### Framework-agnostic vs Rails-coupled checks
|
|
134
|
+
|
|
135
|
+
Checks that depend on Rails internals require those libraries to be present in the stack. Checks that use only stdlib or standalone gems work in any Rack context:
|
|
136
|
+
|
|
137
|
+
| Check | Works without Rails? |
|
|
138
|
+
|-------|---------------------|
|
|
139
|
+
| `:disk` | Yes |
|
|
140
|
+
| `:memory` | Yes |
|
|
141
|
+
| `:http` | Yes |
|
|
142
|
+
| `:redis` | Yes (requires `redis` gem) |
|
|
143
|
+
| `:smtp` | Yes (reads `ActionMailer` config if available, otherwise requires `config.smtp_address`) |
|
|
144
|
+
| `:database` | Requires ActiveRecord |
|
|
145
|
+
| `:cache` | Requires `Rails.cache` |
|
|
146
|
+
| `:sidekiq` | Requires Sidekiq |
|
|
147
|
+
| `:solid_queue` | Requires SolidQueue |
|
|
148
|
+
| `:good_job` | Requires GoodJob |
|
|
149
|
+
| `:resque` | Requires Resque |
|
|
150
|
+
|
|
151
|
+
### Per-environment toggling in Rack
|
|
152
|
+
|
|
153
|
+
`config.disable :check, in: :env` compares against `Rails.env` in a Rails app. In a non-Rails Rack app it reads `ENV["RACK_ENV"]` instead (defaulting to `"production"` if unset):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
config.disable :disk, in: :test # compares RACK_ENV when Rails is not defined
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Authentication in Rack
|
|
160
|
+
|
|
161
|
+
All three authentication strategies work identically. When using the custom block strategy, the argument is a `Rack::Request` instead of `ActionDispatch::Request`:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
RailsHealthChecks.configure do |config|
|
|
165
|
+
config.authenticate { |request| request.env["HTTP_X_INTERNAL"] == "true" }
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Token and IP allowlist strategies are unchanged.
|
|
170
|
+
|
|
171
|
+
[↑ Back to top](#table-of-contents)
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
69
175
|
## Endpoints
|
|
70
176
|
|
|
71
177
|
| Endpoint | Format | Use case |
|
|
@@ -274,7 +380,7 @@ RailsHealthChecks.configure do |config|
|
|
|
274
380
|
end
|
|
275
381
|
```
|
|
276
382
|
|
|
277
|
-
The block receives the
|
|
383
|
+
The block receives the request object and must return a truthy value to allow access. In a Rails app this is `ActionDispatch::Request`; in the Rack app it is `Rack::Request`.
|
|
278
384
|
|
|
279
385
|
[↑ Back to top](#table-of-contents)
|
|
280
386
|
|
|
@@ -46,7 +46,7 @@ module RailsHealthChecks
|
|
|
46
46
|
|
|
47
47
|
def self.run(checks, timeout:)
|
|
48
48
|
results = {}
|
|
49
|
-
|
|
49
|
+
instrument do |payload|
|
|
50
50
|
futures = checks.transform_values do |check|
|
|
51
51
|
t = check.timeout || timeout
|
|
52
52
|
Concurrent::Future.execute { run_check(check, timeout: t) }
|
|
@@ -55,14 +55,26 @@ module RailsHealthChecks
|
|
|
55
55
|
t = check.timeout || timeout
|
|
56
56
|
results[name] = futures[name].value(t + 1) || mark_critical(check, "timed out")
|
|
57
57
|
end
|
|
58
|
-
payload
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if payload
|
|
59
|
+
payload[:status] = overall_status(results)
|
|
60
|
+
payload[:checks] = results.transform_values do |c|
|
|
61
|
+
{ status: c.status, message: c.message, latency_ms: c.latency_ms }.compact
|
|
62
|
+
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
results
|
|
64
66
|
end
|
|
65
67
|
|
|
68
|
+
def self.instrument(&block)
|
|
69
|
+
if defined?(ActiveSupport::Notifications)
|
|
70
|
+
ActiveSupport::Notifications.instrument("health_check.rails_health_checks", &block)
|
|
71
|
+
else
|
|
72
|
+
# :nocov:
|
|
73
|
+
yield nil
|
|
74
|
+
# :nocov:
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
66
78
|
def self.run_check(check, timeout:)
|
|
67
79
|
Timeout.timeout(timeout) { check.call }
|
|
68
80
|
check
|
|
@@ -86,6 +98,6 @@ module RailsHealthChecks
|
|
|
86
98
|
end
|
|
87
99
|
end
|
|
88
100
|
|
|
89
|
-
private_class_method :run_check, :mark_critical, :overall_status
|
|
101
|
+
private_class_method :run_check, :mark_critical, :overall_status, :instrument
|
|
90
102
|
end
|
|
91
103
|
end
|
|
@@ -42,7 +42,8 @@ module RailsHealthChecks
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def checks
|
|
45
|
-
|
|
45
|
+
current_env = defined?(Rails) ? Rails.env.to_s : ENV.fetch("RACK_ENV", "production")
|
|
46
|
+
disabled = @disabled_checks.filter_map { |name, envs| name if envs.include?(current_env) }
|
|
46
47
|
@checks - disabled
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rack"
|
|
6
|
+
|
|
7
|
+
module RailsHealthChecks
|
|
8
|
+
module Rack
|
|
9
|
+
# Mountable Rack app exposing the same endpoints as the Rails engine.
|
|
10
|
+
# Mount in config.ru:
|
|
11
|
+
#
|
|
12
|
+
# require "rails_health_checks/rack/app"
|
|
13
|
+
# map "/health" { run RailsHealthChecks::Rack::App }
|
|
14
|
+
#
|
|
15
|
+
# Or in Sinatra/Roda via their mount helpers.
|
|
16
|
+
#
|
|
17
|
+
# Available routes (relative to mount point):
|
|
18
|
+
# GET/HEAD / → JSON health (same shape as Rails engine)
|
|
19
|
+
# GET/HEAD /live → plain-text liveness probe
|
|
20
|
+
# GET /metrics → Prometheus text format
|
|
21
|
+
# GET /:group → scoped group JSON
|
|
22
|
+
class App
|
|
23
|
+
def self.call(env)
|
|
24
|
+
new(env).call
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(env)
|
|
28
|
+
@env = env
|
|
29
|
+
@request = ::Rack::Request.new(env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call
|
|
33
|
+
return unauthorized unless authorized?
|
|
34
|
+
|
|
35
|
+
response = dispatch
|
|
36
|
+
@request.head? ? [response[0], response[1], []] : response
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dispatch
|
|
40
|
+
path = @request.path_info.delete_suffix("/")
|
|
41
|
+
method = @request.request_method
|
|
42
|
+
|
|
43
|
+
case [method, path]
|
|
44
|
+
when ["GET", ""], ["HEAD", ""]
|
|
45
|
+
health_response
|
|
46
|
+
when ["GET", "/live"], ["HEAD", "/live"]
|
|
47
|
+
live_response
|
|
48
|
+
when ["GET", "/metrics"]
|
|
49
|
+
metrics_response
|
|
50
|
+
else
|
|
51
|
+
if %w[GET HEAD].include?(method) && (m = path.match(%r{\A/([^/]+)\z}))
|
|
52
|
+
group_response(m[1])
|
|
53
|
+
else
|
|
54
|
+
not_found
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def run_checks(check_names)
|
|
62
|
+
config = RailsHealthChecks.configuration
|
|
63
|
+
if config.cache_duration
|
|
64
|
+
cache_key = check_names.map(&:to_s).sort.join(",")
|
|
65
|
+
RailsHealthChecks.result_cache.fetch(cache_key, ttl: config.cache_duration) do
|
|
66
|
+
build_and_run(check_names, config)
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
build_and_run(check_names, config)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_and_run(check_names, config)
|
|
74
|
+
checks = CheckRegistry.build(check_names)
|
|
75
|
+
CheckRegistry.run(checks, timeout: config.timeout)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def health_response
|
|
79
|
+
builder = ResponseBuilder.new(run_checks(RailsHealthChecks.configuration.checks))
|
|
80
|
+
json(builder.http_status == :ok ? 200 : 503, builder.to_json)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def live_response
|
|
84
|
+
builder = ResponseBuilder.new(run_checks(RailsHealthChecks.configuration.checks))
|
|
85
|
+
if builder.overall_status == "ok"
|
|
86
|
+
plain(200, "OK")
|
|
87
|
+
else
|
|
88
|
+
plain(503, "Service Unavailable")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def metrics_response
|
|
93
|
+
results = run_checks(RailsHealthChecks.configuration.checks)
|
|
94
|
+
[200, { "Content-Type" => "text/plain; version=0.0.4" }, [PrometheusFormatter.new(results).to_text]]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def group_response(id)
|
|
98
|
+
group_name = id.to_sym
|
|
99
|
+
check_names = RailsHealthChecks.configuration.groups[group_name]
|
|
100
|
+
return not_found("Group '#{group_name}' not found") unless check_names
|
|
101
|
+
|
|
102
|
+
builder = ResponseBuilder.new(run_checks(check_names))
|
|
103
|
+
json(builder.http_status == :ok ? 200 : 503, builder.to_json)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def authorized?
|
|
107
|
+
config = RailsHealthChecks.configuration
|
|
108
|
+
return true unless config.authenticate_block || config.token || config.allowed_ips
|
|
109
|
+
|
|
110
|
+
if config.authenticate_block
|
|
111
|
+
config.authenticate_block.call(@request)
|
|
112
|
+
elsif config.token
|
|
113
|
+
@env["HTTP_AUTHORIZATION"] == "Bearer #{config.token}"
|
|
114
|
+
elsif config.allowed_ips
|
|
115
|
+
ip_allowed?(config.allowed_ips)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ip_allowed?(allowed_ips)
|
|
120
|
+
client_ip = IPAddr.new(@request.ip)
|
|
121
|
+
allowed_ips.any? { |entry| IPAddr.new(entry).include?(client_ip) }
|
|
122
|
+
rescue IPAddr::InvalidAddressError
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def json(status, body)
|
|
127
|
+
[status, { "Content-Type" => "application/json" }, [body]]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def plain(status, body)
|
|
131
|
+
[status, { "Content-Type" => "text/plain" }, [body]]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def unauthorized
|
|
135
|
+
json(401, { error: "Unauthorized" }.to_json)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def not_found(message = "Not found")
|
|
139
|
+
json(404, { error: message }.to_json)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/rails_health_checks.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails_health_checks/version"
|
|
4
|
-
require "rails_health_checks/engine"
|
|
4
|
+
require "rails_health_checks/engine" if defined?(Rails)
|
|
5
5
|
require "rails_health_checks/configuration"
|
|
6
6
|
require "rails_health_checks/authentication"
|
|
7
7
|
require "rails_health_checks/check"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_health_checks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -15,14 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '7.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: concurrent-ruby
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.1'
|
|
26
40
|
description: A Rails engine that adds structured, pluggable health check endpoints
|
|
27
41
|
to any Rails app. Includes 11 built-in checks (database, cache, Redis, SMTP, Sidekiq,
|
|
28
42
|
SolidQueue, GoodJob, Resque, disk, memory, HTTP), parallel execution via Concurrent::Future,
|
|
@@ -67,6 +81,7 @@ files:
|
|
|
67
81
|
- lib/rails_health_checks/configuration.rb
|
|
68
82
|
- lib/rails_health_checks/engine.rb
|
|
69
83
|
- lib/rails_health_checks/prometheus_formatter.rb
|
|
84
|
+
- lib/rails_health_checks/rack/app.rb
|
|
70
85
|
- lib/rails_health_checks/response_builder.rb
|
|
71
86
|
- lib/rails_health_checks/result_cache.rb
|
|
72
87
|
- lib/rails_health_checks/version.rb
|