philiprehberger-circuit_board

Tests Gem Version Last updated

Health check framework with dependency aggregation and Rack endpoint

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-circuit_board"

Or install directly:

gem install philiprehberger-circuit_board

Usage

require "philiprehberger/circuit_board"

Philiprehberger::CircuitBoard.configure do
  check(:database) { ActiveRecord::Base.connection.active? }
  check(:redis, timeout: 2) { Redis.current.ping == 'PONG' }
end

status = Philiprehberger::CircuitBoard.check
status.healthy?   # => true
status.degraded?  # => false
status.to_h       # => { status: 'healthy', checks: [...] }

Single Check

result = Philiprehberger::CircuitBoard.check_one(:database)
# => { name: :database, healthy: true, duration: 0.0012 }

Raises Philiprehberger::CircuitBoard::Error if the named check does not exist.

Rack Middleware

# config.ru
require "philiprehberger/circuit_board"

Philiprehberger::CircuitBoard.configure do
  check(:database) { DB.connected? }
end

use Philiprehberger::CircuitBoard::Rack::Middleware
run MyApp

Exposes three endpoints:

  • GET /health - full health check with all dependency results
  • GET /health/ready - readiness probe (all checks must pass)
  • GET /health/live - liveness probe (always returns 200)

Critical and Non-Critical Checks

Philiprehberger::CircuitBoard.configure do
  check(:database) { ActiveRecord::Base.connection.active? }          # critical (default)
  check(:cache, critical: false) { Redis.current.ping == 'PONG' }    # non-critical
end

status = Philiprehberger::CircuitBoard.check
# If only cache fails: status is "degraded"
# If database fails:   status is "unhealthy"

Parallel Execution

Run all checks concurrently for lower latency:

status = Philiprehberger::CircuitBoard.check(parallel: true)
status.healthy?          # => true
status.duration          # => wall-clock time of the slowest check
status.unhealthy_checks  # => []
status.to_json           # => '{"status":"healthy","checks":[...]}'

Caching Expensive Checks

Philiprehberger::CircuitBoard.configure do
  # Cache successful database probe for 30 seconds
  check(:database, cache: 30) { ActiveRecord::Base.connection.active? }
end

# First call hits the DB; subsequent calls within 30s return the cached result.
# Failed checks are never cached — failures re-run on every probe.

Check Timeouts

Philiprehberger::CircuitBoard.configure do
  check(:fast_service, timeout: 1) { FastService.ping }
  check(:slow_service, timeout: 10) { SlowService.ping }
end

State Change Callback

require "philiprehberger/circuit_board"

Philiprehberger::CircuitBoard.configure do |c|
  c.check("database") { ActiveRecord::Base.connection.active? }
  c.on_change do |from, to|
    puts "Health changed: #{from} -> #{to}"
  end
end

API

Method Description
.configure { ... } Define health checks using the DSL (check(name, timeout:, critical:, cache:))
.check(parallel: false) Run all checks and return a Status; parallel: true runs concurrently
.check_one(name) Run a single named check and return its result hash
.reset! Remove all configured checks
on_change(&block) Callback invoked on health status transitions
Status#healthy? Whether all checks passed
Status#degraded? Whether some checks passed but not all
Status#to_h Hash with :status and :checks keys
Status#results Array of individual check results
Status#unhealthy_checks Failed check results
Status#healthy_checks Passed check results
Status#duration Wall-clock duration of slowest check
Status#to_json JSON string of to_h
Rack::Middleware.new(app) Rack middleware for health endpoints

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT