standard_health 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 298b48147224460e942fb5dd9440596ef128f0967dc7212a507756b9b45ae1b5
4
+ data.tar.gz: 5008923ddf09d5637f9e6c07b95bf9073b15d0e79b2d0e00646170478e5d7aee
5
+ SHA512:
6
+ metadata.gz: 61f79addc2cba3dc84019036dd38b0917b5c812193b6b8af551b4efdd8578f93499a8efaed5f95b38efa07d71bf5d3bfe19495ff7f54eea145895169289aa7e5
7
+ data.tar.gz: ff348b35a931bed97044118e15b275ac1daed5fae93929c1f2976bc1db1947dbbeb8a05fe8029bc277311e6174e73e8be94c3afc060c880282a1fa1906406d63
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-04-28
11
+
12
+ ### Added
13
+
14
+ - Initial release of `standard_health` — a mountable Rails engine providing `/alive`, `/ready`, and `/diagnostics/env` endpoints.
15
+ - `StandardHealth.configure` block with `register_check`, `parent_controller`, and `env_spec` accessors.
16
+ - `StandardHealth::EnvSpec` DSL for declaring required and recommended environment variables, with optional per-mode applicability via `in:`.
17
+ - Built-in checks: `Checks::ActiveRecord`, `Checks::SolidQueue`, `Checks::SolidCache`.
18
+ - `StandardHealth::Aggregator` rolls registered checks into `:ok` / `:degraded` / `:unavailable` overall status.
19
+ - `StandardHealth::Check` base class with a `with_timing` helper for subclasses.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Jaryl Sim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # StandardHealth
2
+
3
+ A drop-in health check and environment-spec engine for Rails 8 host apps.
4
+
5
+ Mount it once and you get:
6
+
7
+ - `GET /health/alive` — liveness probe (always 200 if Rails is up)
8
+ - `GET /health/ready` — readiness probe; runs every registered check and rolls them up into an overall status
9
+ - `GET /health/diagnostics/env` — audits the host app's `ENV` against a declarative spec
10
+
11
+ Built-in checks cover ActiveRecord, SolidQueue, and SolidCache. Host apps can register additional checks via the configuration block.
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "standard_health"
19
+ ```
20
+
21
+ Then `bundle install`.
22
+
23
+ ## Mounting
24
+
25
+ In `config/routes.rb`:
26
+
27
+ ```ruby
28
+ mount StandardHealth::Engine => "/health"
29
+ ```
30
+
31
+ This wires up:
32
+
33
+ - `GET /health/alive`
34
+ - `GET /health/ready`
35
+ - `GET /health/diagnostics/env`
36
+
37
+ ## Configuration
38
+
39
+ Create `config/initializers/standard_health.rb`:
40
+
41
+ ```ruby
42
+ StandardHealth.configure do |c|
43
+ # Controllers under StandardHealth inherit from this class. Use a host
44
+ # app controller to apply auth before_actions to every endpoint.
45
+ c.parent_controller = "ApplicationController"
46
+
47
+ # Register checks. The first argument is a short name surfaced in JSON;
48
+ # `critical: true` means a failure flips overall status to :unavailable.
49
+ c.register_check :database, StandardHealth::Checks::ActiveRecord, critical: true
50
+ c.register_check :solid_queue, StandardHealth::Checks::SolidQueue, critical: true
51
+ c.register_check :solid_cache, StandardHealth::Checks::SolidCache, critical: false
52
+
53
+ # Declare the env vars your app expects.
54
+ c.env_spec = StandardHealth::EnvSpec.define do
55
+ required :SECRET_KEY_BASE
56
+ required :APP_ENVIRONMENT, in: %w[staging production]
57
+ required :DATABASE_URL, in: %w[production]
58
+ recommended :SENTRY_DSN, description: "Error tracking DSN"
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## EnvSpec
64
+
65
+ The DSL has two declarations:
66
+
67
+ - `required :NAME` — missing value reports `status: :missing`
68
+ - `recommended :NAME` — missing value reports `status: :should_set`
69
+
70
+ Both accept:
71
+
72
+ - `in: %w[staging production]` — restricts the entry to those `APP_ENVIRONMENT` values; ignored otherwise
73
+ - `description: "..."` — surfaced verbatim in the audit JSON
74
+
75
+ Audit output (one row per applicable entry):
76
+
77
+ ```json
78
+ {
79
+ "name": "SECRET_KEY_BASE",
80
+ "level": "required",
81
+ "status": "ok",
82
+ "mode": "production"
83
+ }
84
+ ```
85
+
86
+ Possible `status` values are `ok`, `missing` (required + absent), and `should_set` (recommended + absent).
87
+
88
+ ## Custom checks
89
+
90
+ Inherit from `StandardHealth::Check` and implement `#run`:
91
+
92
+ ```ruby
93
+ class RedisCheck < StandardHealth::Check
94
+ def run
95
+ with_timing { Redis.current.ping }
96
+ end
97
+ end
98
+
99
+ StandardHealth.configure do |c|
100
+ c.register_check :redis, RedisCheck, critical: false
101
+ end
102
+ ```
103
+
104
+ `with_timing` captures `latency_ms` on success and converts any `StandardError` into `{ status: :fail, error: <message> }`.
105
+
106
+ ## Auth
107
+
108
+ `/alive` and `/ready` are typically left open for orchestrator probes. `/diagnostics/env` enumerates which env vars are missing — that's potentially sensitive, so the host app is responsible for protecting it.
109
+
110
+ The recommended pattern is to point `parent_controller` at a host app controller that enforces auth:
111
+
112
+ ```ruby
113
+ # app/controllers/internal_health_controller.rb
114
+ class InternalHealthController < ActionController::API
115
+ http_basic_authenticate_with(
116
+ name: ENV.fetch("HEALTH_USER"),
117
+ password: ENV.fetch("HEALTH_PASS"),
118
+ only: :env # only protect diagnostics
119
+ )
120
+ end
121
+
122
+ # config/initializers/standard_health.rb
123
+ StandardHealth.configure do |c|
124
+ c.parent_controller = "InternalHealthController"
125
+ end
126
+ ```
127
+
128
+ For a more granular setup, mount the engine inside an authenticated route block in your host app's `routes.rb`.
129
+
130
+ ## Status semantics
131
+
132
+ `/ready` returns:
133
+
134
+ | Overall status | HTTP code | Meaning |
135
+ |---|---|---|
136
+ | `ok` | 200 | All checks passed |
137
+ | `degraded` | 200 | A non-critical check failed |
138
+ | `unavailable` | 503 | A critical check failed |
139
+
140
+ The orchestrator should pull the instance out of rotation only on 503; degraded means "still serving, page someone."
141
+
142
+ ## License
143
+
144
+ MIT.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake" if File.exist?(APP_RAKEFILE)
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ begin
11
+ require "rspec/core/rake_task"
12
+ RSpec::Core::RakeTask.new(:spec)
13
+ task default: :spec
14
+ rescue LoadError
15
+ # rspec not available — skip
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # Base controller for all StandardHealth endpoints.
5
+ #
6
+ # Lazily inherits from `StandardHealth.config.parent_controller` so host
7
+ # apps can wire their own auth/rate-limiting/before_actions in once and
8
+ # have them apply to /alive, /ready, and /diagnostics/env. The default
9
+ # is `ActionController::API` so the engine works in API-only host apps
10
+ # without configuration.
11
+ #
12
+ # Resolution happens at the moment this class is first referenced, which
13
+ # is request time — by then the host app's controllers are loaded and
14
+ # the constant resolves cleanly. Caching the resolved class on the
15
+ # singleton avoids re-running `constantize` per request.
16
+ def self.parent_controller
17
+ @parent_controller ||= config.parent_controller.constantize
18
+ end
19
+
20
+ # Allow tests/host apps to bust the cached parent controller (e.g. when
21
+ # toggling config between examples).
22
+ def self.reset_parent_controller!
23
+ @parent_controller = nil
24
+ end
25
+
26
+ class ApplicationController < parent_controller
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # Diagnostics endpoints. Output here is potentially sensitive (it
5
+ # enumerates which env vars are missing), so host apps are responsible
6
+ # for wrapping these routes with authentication via `parent_controller`
7
+ # — typically a basic-auth `before_action` on `ApplicationController`.
8
+ class DiagnosticsController < ApplicationController
9
+ # Audits the configured EnvSpec against the current process ENV and
10
+ # returns the result as JSON. When no EnvSpec is configured the
11
+ # endpoint returns an empty audit rather than a 404 so callers don't
12
+ # have to special-case "feature not enabled".
13
+ def env
14
+ spec = StandardHealth.config.env_spec
15
+ mode = ENV["APP_ENVIRONMENT"].to_s
16
+
17
+ audit = spec ? spec.audit(ENV.to_h, mode: mode) : []
18
+
19
+ render json: {
20
+ mode: mode,
21
+ audit: audit,
22
+ generated_at: Time.now.utc.iso8601
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ class HealthController < ApplicationController
5
+ # Liveness probe. Returns 200 unconditionally — its only job is to
6
+ # confirm the Rails process is up and routing requests. Anything
7
+ # heavier belongs in /ready.
8
+ def alive
9
+ head :ok
10
+ end
11
+
12
+ # Readiness probe. Runs every registered check and returns:
13
+ # 200 if the rolled-up status is :ok or :degraded
14
+ # 503 if any critical check failed (:unavailable)
15
+ def ready
16
+ result = StandardHealth::Aggregator.call
17
+ http_status = result[:status] == :unavailable ? :service_unavailable : :ok
18
+ render json: result, status: http_status
19
+ end
20
+ end
21
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ StandardHealth::Engine.routes.draw do
4
+ get "/alive", to: "health#alive"
5
+ get "/ready", to: "health#ready"
6
+
7
+ namespace :diagnostics do
8
+ get :env
9
+ end
10
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # Runs all registered checks and rolls them up into a single status.
5
+ #
6
+ # Status semantics:
7
+ # :ok — every check returned :ok
8
+ # :degraded — at least one non-critical check failed
9
+ # :unavailable — at least one critical check failed
10
+ #
11
+ # The aggregator never raises. Each check is invoked through
12
+ # `safe_run` which catches `StandardError` so a buggy custom check
13
+ # cannot take down /ready.
14
+ class Aggregator
15
+ def self.call(checks: StandardHealth.config.checks, now: Time.now.utc)
16
+ check_rows = checks.map { |reg| safe_run(reg) }
17
+ {
18
+ status: overall_status(check_rows),
19
+ checks: check_rows,
20
+ generated_at: now.iso8601
21
+ }
22
+ end
23
+
24
+ def self.safe_run(reg)
25
+ result = reg.klass.new(name: reg.name, critical: reg.critical).run
26
+ result.merge(name: reg.name, critical: reg.critical)
27
+ rescue StandardError => e
28
+ {
29
+ name: reg.name,
30
+ critical: reg.critical,
31
+ status: :fail,
32
+ error: e.message
33
+ }
34
+ end
35
+ private_class_method :safe_run
36
+
37
+ def self.overall_status(rows)
38
+ return :ok if rows.empty?
39
+
40
+ failures = rows.reject { |r| r[:status] == :ok }
41
+ return :ok if failures.empty?
42
+ return :unavailable if failures.any? { |r| r[:critical] }
43
+
44
+ :degraded
45
+ end
46
+ private_class_method :overall_status
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # Base class for health checks.
5
+ #
6
+ # Subclasses implement `#run`, returning a hash like:
7
+ #
8
+ # { status: :ok, latency_ms: 3 }
9
+ #
10
+ # or
11
+ #
12
+ # { status: :fail, error: "connection refused" }
13
+ #
14
+ # The `with_timing` helper wraps a block, captures latency, and converts
15
+ # any unhandled `StandardError` into a `:fail` row so subclasses don't
16
+ # have to repeat the pattern.
17
+ class Check
18
+ attr_reader :name, :critical
19
+
20
+ def initialize(name:, critical: false)
21
+ @name = name
22
+ @critical = critical
23
+ end
24
+
25
+ def critical?
26
+ !!@critical
27
+ end
28
+
29
+ # Subclasses override this. Default implementation reports an
30
+ # unimplemented check rather than raising, so a misconfigured custom
31
+ # check degrades gracefully instead of taking down /ready.
32
+ def run
33
+ { status: :fail, error: "not implemented" }
34
+ end
35
+
36
+ # Wrap a block: time it, return :ok with latency_ms on success, or
37
+ # :fail with the error message on any StandardError. Useful for
38
+ # check implementations that boil down to "run this query, swallow
39
+ # exceptions, report status".
40
+ def with_timing
41
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
+ yield
43
+ latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
44
+ { status: :ok, latency_ms: latency_ms }
45
+ rescue StandardError => e
46
+ { status: :fail, error: e.message }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_health/check"
4
+
5
+ module StandardHealth
6
+ module Checks
7
+ # Verifies the primary database connection by executing `SELECT 1`.
8
+ # Critical by default — if the database is unreachable, the host app
9
+ # cannot serve meaningful traffic.
10
+ class ActiveRecord < Check
11
+ def initialize(name: :active_record, critical: true)
12
+ super
13
+ end
14
+
15
+ def run
16
+ with_timing do
17
+ ::ActiveRecord::Base.connection.execute("SELECT 1")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_health/check"
4
+
5
+ module StandardHealth
6
+ module Checks
7
+ # Verifies the Rails cache is reachable via a read-only probe.
8
+ #
9
+ # We deliberately don't write — a flapping cache shouldn't corrupt host
10
+ # app cache state. If the read raises (connection refused, decoding
11
+ # error, etc.), the check fails. Non-critical by default: a degraded
12
+ # cache shouldn't pull the app out of rotation.
13
+ class SolidCache < Check
14
+ PROBE_KEY = "standard_health_probe"
15
+
16
+ def initialize(name: :solid_cache, critical: false)
17
+ super
18
+ end
19
+
20
+ def run
21
+ with_timing do
22
+ ::Rails.cache.read(PROBE_KEY)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_health/check"
4
+
5
+ module StandardHealth
6
+ module Checks
7
+ # Verifies the SolidQueue backing store is reachable.
8
+ #
9
+ # SolidQueue can be configured to live on a dedicated database connection
10
+ # (`config.solid_queue.connects_to`). When that's the case we run the
11
+ # probe against `SolidQueue::Record.connection`. Otherwise we fall back
12
+ # to the primary AR connection — which is exactly where the queue tables
13
+ # live in single-DB setups.
14
+ class SolidQueue < Check
15
+ def initialize(name: :solid_queue, critical: true)
16
+ super
17
+ end
18
+
19
+ def run
20
+ with_timing do
21
+ connection.execute("SELECT 1")
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def connection
28
+ if defined?(::SolidQueue::Record) && ::SolidQueue::Record.respond_to?(:connection)
29
+ ::SolidQueue::Record.connection
30
+ else
31
+ ::ActiveRecord::Base.connection
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # Holds engine-wide configuration.
5
+ #
6
+ # Host apps configure the engine via:
7
+ #
8
+ # StandardHealth.configure do |c|
9
+ # c.parent_controller = "ApplicationController"
10
+ # c.register_check :custom, MyCheck, critical: true
11
+ # c.env_spec = StandardHealth::EnvSpec.define { ... }
12
+ # end
13
+ class Configuration
14
+ # A registered health check entry.
15
+ Registration = Struct.new(:name, :klass, :critical, keyword_init: true) do
16
+ def critical?
17
+ !!critical
18
+ end
19
+ end
20
+
21
+ # Class name of the controller that StandardHealth's controllers should
22
+ # inherit from. Resolved lazily via `constantize` at request time so the
23
+ # host app's controller (which may pull in auth concerns) is fully
24
+ # loaded before we touch it. Defaults to `ActionController::API` so the
25
+ # engine works in API-only host apps without configuration.
26
+ attr_accessor :parent_controller
27
+
28
+ # An optional `StandardHealth::EnvSpec` instance describing required and
29
+ # recommended environment variables for the host app. Audited via the
30
+ # /diagnostics/env endpoint.
31
+ attr_accessor :env_spec
32
+
33
+ def initialize
34
+ @parent_controller = "ActionController::API"
35
+ @env_spec = nil
36
+ @checks = []
37
+ end
38
+
39
+ # Register a health check class.
40
+ #
41
+ # @param name [Symbol] short identifier surfaced in /ready output
42
+ # @param klass [Class] subclass of StandardHealth::Check
43
+ # @param critical [Boolean] failure flips overall status to :unavailable
44
+ def register_check(name, klass, critical: false)
45
+ @checks << Registration.new(name: name.to_sym, klass: klass, critical: critical)
46
+ end
47
+
48
+ # @return [Array<Registration>] frozen view of registered checks
49
+ def checks
50
+ @checks.dup
51
+ end
52
+
53
+ # Remove all registered checks. Mainly useful in tests where the host
54
+ # app and the engine share a process.
55
+ def reset_checks!
56
+ @checks = []
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace StandardHealth
6
+ end
7
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ # DSL for declaring required and recommended environment variables.
5
+ #
6
+ # Example:
7
+ #
8
+ # StandardHealth::EnvSpec.define do
9
+ # required :SECRET_KEY_BASE
10
+ # required :APP_ENVIRONMENT, in: %w[staging production]
11
+ # recommended :SENTRY_DSN, description: "Error tracking DSN"
12
+ # end
13
+ #
14
+ # Each entry has:
15
+ # - `name` (Symbol)
16
+ # - `level` (:required | :recommended)
17
+ # - `modes` (Array<String>, optional) — when set, the entry only applies
18
+ # while `APP_ENVIRONMENT` matches one of these modes. Otherwise it
19
+ # applies to every mode.
20
+ # - `description` (String, optional) — human-readable hint surfaced
21
+ # verbatim by `/diagnostics/env`.
22
+ class EnvSpec
23
+ Entry = Struct.new(:name, :level, :modes, :description, keyword_init: true) do
24
+ # Whether this entry's audit applies under the given runtime mode.
25
+ # Entries without a `modes:` constraint always apply.
26
+ def applies_to?(mode)
27
+ return true if modes.nil? || modes.empty?
28
+ modes.include?(mode.to_s)
29
+ end
30
+ end
31
+
32
+ # @return [Array<Entry>]
33
+ attr_reader :entries
34
+
35
+ # Build a spec via the DSL.
36
+ def self.define(&block)
37
+ new.tap { |spec| spec.instance_eval(&block) if block }
38
+ end
39
+
40
+ def initialize
41
+ @entries = []
42
+ end
43
+
44
+ # Declare a required env var.
45
+ #
46
+ # @param name [Symbol, String]
47
+ # @param in [Array<String>, nil] limit applicability to these modes
48
+ # @param description [String, nil]
49
+ def required(name, **opts)
50
+ add(:required, name, **opts)
51
+ end
52
+
53
+ # Declare a recommended env var. A missing value never fails the audit;
54
+ # it surfaces as `:should_set`.
55
+ def recommended(name, **opts)
56
+ add(:recommended, name, **opts)
57
+ end
58
+
59
+ # Run the audit against an env-like hash.
60
+ #
61
+ # @param env_hash [Hash{String, Symbol => String}] e.g. ENV.to_h
62
+ # @param mode [String, Symbol] current APP_ENVIRONMENT value
63
+ # @return [Array<Hash>] one row per applicable entry, each shaped like:
64
+ # { name:, level:, status: :ok | :missing | :should_set, mode: }
65
+ def audit(env_hash, mode:)
66
+ mode_str = mode.to_s
67
+ env = stringify(env_hash)
68
+
69
+ @entries.each_with_object([]) do |entry, out|
70
+ next unless entry.applies_to?(mode_str)
71
+
72
+ value = env[entry.name.to_s]
73
+ status = classify(entry, value)
74
+
75
+ row = {
76
+ name: entry.name,
77
+ level: entry.level,
78
+ status: status,
79
+ mode: mode_str
80
+ }
81
+ row[:description] = entry.description if entry.description
82
+ out << row
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def add(level, name, in: nil, description: nil)
89
+ modes = binding.local_variable_get(:in)
90
+ @entries << Entry.new(
91
+ name: name.to_sym,
92
+ level: level,
93
+ modes: modes ? Array(modes).map(&:to_s) : nil,
94
+ description: description
95
+ )
96
+ end
97
+
98
+ def classify(entry, value)
99
+ present = !value.nil? && !value.to_s.empty?
100
+ return :ok if present
101
+
102
+ entry.level == :required ? :missing : :should_set
103
+ end
104
+
105
+ def stringify(env_hash)
106
+ env_hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardHealth
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_health/version"
4
+ require "standard_health/engine"
5
+ require "standard_health/configuration"
6
+ require "standard_health/env_spec"
7
+ require "standard_health/check"
8
+ require "standard_health/checks/active_record"
9
+ require "standard_health/checks/solid_queue"
10
+ require "standard_health/checks/solid_cache"
11
+ require "standard_health/aggregator"
12
+
13
+ module StandardHealth
14
+ class << self
15
+ # Yields the configuration to a block.
16
+ #
17
+ # StandardHealth.configure do |c|
18
+ # c.parent_controller = "ApplicationController"
19
+ # c.register_check :db, StandardHealth::Checks::ActiveRecord, critical: true
20
+ # end
21
+ def configure
22
+ yield config if block_given?
23
+ config
24
+ end
25
+
26
+ def config
27
+ @config ||= Configuration.new
28
+ end
29
+
30
+ # Mostly useful in tests — wipes the singleton config so each example
31
+ # gets a clean slate.
32
+ def reset_config!
33
+ @config = Configuration.new
34
+ end
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: standard_health
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jaryl Sim
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ description: StandardHealth is a mountable Rails engine providing /alive, /ready,
27
+ and /diagnostics/env endpoints, with a configuration block for registering custom
28
+ checks and a DSL for declaring required and recommended environment variables.
29
+ email:
30
+ - code@jaryl.dev
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - MIT-LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - app/controllers/standard_health/application_controller.rb
40
+ - app/controllers/standard_health/diagnostics_controller.rb
41
+ - app/controllers/standard_health/health_controller.rb
42
+ - config/routes.rb
43
+ - lib/standard_health.rb
44
+ - lib/standard_health/aggregator.rb
45
+ - lib/standard_health/check.rb
46
+ - lib/standard_health/checks/active_record.rb
47
+ - lib/standard_health/checks/solid_cache.rb
48
+ - lib/standard_health/checks/solid_queue.rb
49
+ - lib/standard_health/configuration.rb
50
+ - lib/standard_health/engine.rb
51
+ - lib/standard_health/env_spec.rb
52
+ - lib/standard_health/version.rb
53
+ homepage: https://github.com/rarebit-one/standard_health
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://github.com/rarebit-one/standard_health
58
+ source_code_uri: https://github.com/rarebit-one/standard_health
59
+ changelog_uri: https://github.com/rarebit-one/standard_health/blob/main/CHANGELOG.md
60
+ bug_tracker_uri: https://github.com/rarebit-one/standard_health/issues
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.7
76
+ specification_version: 4
77
+ summary: A drop-in health check and environment-spec engine for Rails 8 host apps.
78
+ test_files: []