Skip to content
wiki edited this page Sep 4, 2026 · 1 revision

rextension-health

Liveness, readiness, dependency state tracking, circuit breakers and per-route dependency gating for Rex.

go get github.com/kryovyx/rextension-health
import (
	"github.com/kryovyx/rex"
	health "github.com/kryovyx/rextension-health"
)

app := rex.New(
	health.WithHealth(health.NewConfig(
		health.WithCheck(health.NewCheck("database", checkDatabase,
			health.WithReadiness(true),
			health.WithCheckMode(health.CheckModeActive),
		)),
	)),
)

Out of the box that gives you /live, /ready and /status on a dedicated listener at :9091, a check running every 10 seconds, and a dependency state store the rest of your code can report into.

The four things it does

Endpoints/live, /ready, /status, on their own port so they stay answerable when the application listener is saturated.

Health Checks — named checks with timeouts, tags, readiness flags, and three execution modes (active polling, passive with caching, on-demand).

Dependency State — a store of UP / DEGRADED / DOWN per dependency, fed by checks, by your own ReportSuccess/ReportFailure calls, or by a wrapped HTTP client.

The Dependency Gate — routes declare what they need; requests to a route whose hard dependency is down are refused with a 503 before the handler runs.

Plus Circuit Breakers, which stop hammering a dependency that is already failing.

Sixty seconds

// 1. A check that reports into the state store.
dbCheck := health.NewCheck("database",
	func(ctx context.Context, r rx.Resolver) *health.CheckResult {
		var db *sql.DB
		if err := r.Resolve(&db); err != nil {
			return health.NewCheckResult(health.StatusDown, "unresolvable", 0)
		}
		start := time.Now()
		if err := db.PingContext(ctx); err != nil {
			return health.NewCheckResult(health.StatusDown, "ping failed", time.Since(start))
		}
		return health.NewCheckResult(health.StatusUp, "", time.Since(start))
	},
	health.WithReadiness(true),
	health.WithCheckMode(health.CheckModeActive),
	health.WithTimeout(2*time.Second),
)

app := rex.New(health.WithHealth(health.NewConfig(health.WithCheck(dbCheck))))

// 2. A route that refuses to serve without the database.
app.RegisterRoute(health.NewRouteWithDeps("GET", "/users/{id}", getUser,
	health.NewHardRequirement("database"),
))
$ curl :9091/ready
{"status":"UP","checks":{"database":{"status":"UP","duration":1200000,…}}}

Design decisions worth knowing up front

The gate serves when it does not know. TreatUnknownAs defaults to StatusUp. Availability-gating that fails closed 503s every request for the first check interval after every boot — a rolling deploy then fails its own readiness probes and rolls back. What makes that safe is the synchronous check pass in OnStart: every registered check runs once before the listeners bind, so by the time a request can arrive the states are real. See The Dependency Gate.

Configuration is used verbatim. A non-nil Config is not merged with the defaults field by field. Build it with NewConfig and the With* options — a partial struct literal leaves the rest at their zero values, not their defaults. See Configuration.

Checks are declared in the config, not registered before Run. The registry does not exist until the extension's OnInitialize runs, so reaching into the container for it beforehand resolves nothing. Use WithCheck / WithChecks.

The client never learns which dependency failed. The 503's detail is deliberately generic; the identifier and its state go to the log. Naming it would map your internal service topology for anyone probing endpoints during an outage.

Clone this wiki locally