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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1ee34c7b5d51ecbfbd7f1f8571daad21f2aea68d68b6ae3abb2013692e4d25b
4
- data.tar.gz: 067bba6faa5b8276e1d7e190e5ddeaac050cbf218d8093b00ef865492c640560
3
+ metadata.gz: 934bfd1962ca10009ba6925f81cbf57f09d8d63ab017a6452296bfb32994352b
4
+ data.tar.gz: 7ba0cfd783e433b26de67d2f9a2dc00a13e341584c88b1559c5f08e1f237187c
5
5
  SHA512:
6
- metadata.gz: 7b224e9749a4cecbe92353f278f6d162ee8cca979301754d887f97d76bb004b4ef8305a2cc93aa2ecb7aff21cb7f3bf1e4381daa1ca8f17a32841c19019f14de
7
- data.tar.gz: 5c514cdf7dff0471562ad1b4fc549348ea49b2ee1b816fdb730962e9eb3688b94a4750185f06d7cf16102ce00eea326a58be599edcea05b34c6998e511ce0fca
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 `ActionDispatch::Request` object and must return a truthy value to allow access.
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
- ActiveSupport::Notifications.instrument("health_check.rails_health_checks") do |payload|
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[:status] = overall_status(results)
59
- payload[:checks] = results.transform_values do |c|
60
- { status: c.status, message: c.message, latency_ms: c.latency_ms }.compact
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
- disabled = @disabled_checks.filter_map { |name, envs| name if envs.include?(Rails.env.to_s) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsHealthChecks
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -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.1
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: '8.0'
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: '8.0'
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