-
Notifications
You must be signed in to change notification settings - Fork 0
Health Checks
type HealthCheck interface {
Name() string
Execute(ctx context.Context) *CheckResult
Timeout() time.Duration
IsReadiness() bool
Tags() []string
Mode() CheckMode
CacheTTL() time.Duration
}You rarely implement it. health.NewCheck builds one from a function.
check := 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, "not wired", 0)
}
start := time.Now()
if err := db.PingContext(ctx); err != nil {
// The message is internal-facing — it appears on /status, which
// should not be public. Do not put it in a client response.
return health.NewCheckResult(health.StatusDown, err.Error(), time.Since(start))
}
return health.NewCheckResult(health.StatusUp, "", time.Since(start))
},
health.WithReadiness(true),
health.WithCheckMode(health.CheckModeActive),
health.WithTimeout(2*time.Second),
health.WithTags("db", "critical"),
)The rx.Resolver is the DI resolver, so a check reaches whatever the
application registered without capturing it at construction time.
Respect ctx. It carries the check's timeout. A check that ignores it can
hold up the synchronous startup pass and every subsequent tick.
Declare checks in the configuration:
app := rex.New(health.WithHealth(health.NewConfig(
health.WithCheck(dbCheck),
health.WithChecks(cacheCheck, queueCheck),
)))You cannot resolve the registry and register before
Run. It does not exist until the extension'sOnInitializeruns. Applications used to reach into the container for it; that now resolves nothing.
For wiring that needs more than a check — a route that depends on the state
store, say — write a small application extension and do it in its
OnInitialize:
type wiring struct{ rex.DefaultExtension; h *health.HealthExtension }
func (w *wiring) OnInitialize(ctx context.Context, r rex.Rex) error {
w.h.RegisterCheckFunc("queue", checkQueue, health.WithReadiness(true))
return nil
}| Mode | Runs | Caches | For |
|---|---|---|---|
CheckModeActive |
on a ticker, every CheckInterval
|
— writes to the state store | critical dependencies that must be monitored continuously |
CheckModePassive |
on demand, when a gate needs it | for CacheTTL (30s default) |
optional or expensive dependencies |
CheckModeOnDemand |
on demand, every time | never (CacheTTL forced to 0) |
checks that must be fresh at the moment of use |
Active is the default and the right choice for anything a route hard-depends on: the state is already known when a request arrives, so the gate costs a map read.
Passive trades that for not polling something expensive. The first request after the TTL expires pays for the check.
CheckInterval is 10 seconds by default; WithCheckInterval(0) disables the
ticker entirely, leaving only on-demand execution.
health.WithReadiness(true)Readiness checks are the ones /ready runs. Mark a check as readiness when the
replica cannot usefully serve without it. A check on an optional cache
should not remove the replica from the load balancer.
Every check, readiness or not, appears on /status.
type CheckResult struct {
Status Status // UP | DEGRADED | DOWN | UNKNOWN
Message string
Duration time.Duration
Timestamp time.Time
Metadata map[string]string
}Use DEGRADED rather than DOWN for "working, but badly" — high latency, a
replica lagging, a cache miss rate that has gone wrong. Status.IsHealthy() is
true for UP and DEGRADED, /ready returns 200 for both, and the
gate can still be told to require UP with
WithMinStatus.
health.WithTimeout(2 * time.Second)Bounds one execution. Keep it well below CheckInterval, and remember that the
synchronous startup pass runs every check once before the listeners bind —
so the worst case added to your boot time is roughly the sum of the timeouts of
checks that hang.
health.WithTags("db", "critical")
registry.ExecuteByTags(ctx, "critical")Useful for a subset probe or an operator endpoint that exercises one group.
If something else already reports into the
state store — a wrapped HTTP client, your own
ReportFailure calls — you do not need a polling check. Expose the state as a
check instead:
health.NewDependencyCheck("payments", "payments-api", stateStore, true, "external")It reads the store rather than performing an operation, so it costs nothing and reflects real traffic instead of a synthetic ping.
ext.Registry() gives you the live registry:
Register / Unregister / Get / GetAll
GetByTags / GetReadinessChecks / GetActiveChecks / GetPassiveChecks
ExecuteAll / ExecuteReadiness / ExecuteByTags / ExecuteCheck
Start(interval, stateStore) / Stop
SetResolver / SetLoggerThe extension calls Start and Stop for you around the application lifecycle.
rextension-health — health, readiness and dependency gating for Rex · MIT · © 2026 Kryovyx