Health
- Start here: Health Checks guide
- FastAPI integration:
health_routerfor liveness, readiness, and health endpoints. - Every other framework:
health_asgimounts the same endpoints as a pure-ASGI app, andOpsServerserves them on a port of its own.
grelmicro.health
Health Checks.
Each name loads its submodule on first access.
HealthCheckFunc
HealthCheckFunc = SyncHealthCheckFunc | AsyncHealthCheckFunc
Any callable acceptable as a health check.
Returns:
- None: healthy, no details.
- HealthDetails: healthy, with a details dict.
Raises:
- HealthError: unhealthy. The message surfaces in the response.
- Any other exception: unhealthy with a generic message. The
traceback is logged server-side.
HealthDetails
HealthDetails = dict[str, JSONEncodable]
Per-check details payload. JSON-serializable dict keyed by string.
ProviderChecks
ProviderChecks = Literal['off', 'report', 'require']
Which check HealthChecks registers for each Provider on the app.
"off" registers none. "report" registers a non-critical
provider:{short_name} check, shown in /healthz and left out of
/readyz. "require" registers a critical one, so a down backend fails
/readyz.
CheckResult
Bases: TypedDict
Result of a single health check.
critical
instance-attribute
critical: bool
error
instance-attribute
error: str | None
HealthError
HealthError(
message: str, *, details: HealthDetails | None = None
)
Bases: GrelmicroError
Signal a check failure. The message is exposed in the response.
Pass details to include a diagnostic payload alongside the
error, visible under details on the check entry in
/healthz (subject to show_details).
Initialize with a message and optional details dict.
details
instance-attribute
details = details
HealthChecks
HealthChecks(
*,
name: str = "default",
timeout: PositiveFloat | None = None,
cache_ttl: int | timedelta | None = None,
liveness: Liveness | None = None,
provider_checks: ProviderChecks = "report",
env_prefix: str | None = None,
env_load: bool | None = None,
)
HealthChecks(
*,
name: str = "default",
timeout: PositiveFloat | None = None,
cache_ttl: int | timedelta | None = None,
liveness: Liveness | None = None,
provider_checks: None,
env_prefix: str | None = None,
env_load: bool | None = None,
)
HealthChecks(
*,
name: str = "default",
timeout: PositiveFloat | None = None,
cache_ttl: int | timedelta | None = None,
liveness: Liveness | None = None,
provider_checks: ProviderChecks | None = "report",
env_prefix: str | None = None,
env_load: bool | None = None,
)
Bases: Reconfigurable[HealthChecksConfig]
Manages health checks and runs them concurrently.
Checks are plain async functions. Register them with the
:meth:check decorator or the :meth:add method. All registered
checks are executed in parallel via an asyncio.TaskGroup. Each
check has its own timeout (falling back to the default)
and its own cached result. Concurrent requests for the same check
share a single execution via an asyncio.Event.
Supports live reconfiguration via
reconfigure(new_config).
A swap takes effect on the next :meth:run. In-flight rounds
keep the cache_ttl they started with. The new default
timeout applies to checks registered after the swap.
Existing checks keep the timeout they were registered with.
Re-register a check to pick up the new default. See
Live reconfiguration.
Initialize the health checks.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
Registration name. Multiple
TYPE:
|
timeout
|
Default per-check timeout in seconds. Checks that
exceed this duration are reported as Default: 5.0. When unset and env reads are enabled (see
TYPE:
|
cache_ttl
|
Per-check cache TTL, in whole seconds or as a
Default: 1 second. When unset and env reads are enabled (see
TYPE:
|
liveness
|
When the worker counts as stuck, and exits so it is
replaced.
TYPE:
|
provider_checks
|
Which check to register for each
TYPE:
|
env_prefix
|
Override the auto-derived environment variable prefix. Default:
TYPE:
|
env_load
|
Whether to read environment variables. When None (the default), follow the process-wide
Pass False when the values here are the whole truth. Env reads fill every field not passed, so a config half taken from somewhere else silently gets the rest from the environment.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SettingsValidationError
|
If |
kind
class-attribute
kind: str = 'health'
name
property
name: str
Return the registration name.
is_alive
property
is_alive: bool
Whether the last round of liveness checks passed.
/livez answers 503 while this is false. It is also false once
the liveness checks stopped on an unexpected error, which stops the
worker. It is always true when no Liveness is set.
from_config
classmethod
from_config(
config: HealthChecksConfig,
*,
name: str = "default",
provider_checks: ProviderChecks = "report",
liveness: Liveness | None = None,
) -> Self
from_config(
config: HealthChecksConfig,
*,
name: str = "default",
provider_checks: None,
liveness: Liveness | None = None,
) -> Self
from_config(
config: HealthChecksConfig,
*,
name: str = "default",
provider_checks: ProviderChecks | None = "report",
liveness: Liveness | None = None,
) -> Self
Construct a HealthChecks from a pre-built HealthChecksConfig.
| PARAMETER | DESCRIPTION |
|---|---|
config
|
The pre-built health checks configuration. Use this path when the configuration is assembled at
startup from a settings tree (for example YAML, Vault,
or a
TYPE:
|
name
|
Registration name. Defaults to
TYPE:
|
provider_checks
|
Which check to register for each active
TYPE:
|
liveness
|
When the worker counts as stuck, and exits so it is replaced.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
SettingsValidationError
|
If |
add
add(
name: str,
func: HealthCheckFunc,
*,
critical: bool = True,
timeout: PositiveFloat | None = None,
liveness: bool = False,
) -> None
Register a health check function.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
Unique name identifying this check.
TYPE:
|
func
|
Async function: returns
TYPE:
|
critical
|
Whether this check affects the aggregate status and HTTP response code. Critical failures flip the aggregate to
TYPE:
|
timeout
|
Per-check timeout override. Falls back to the default when omitted.
TYPE:
|
liveness
|
Run this check for
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
check
check(
name: str,
*,
critical: bool = True,
timeout: PositiveFloat | None = None,
liveness: bool = False,
) -> Callable[[FuncT], FuncT]
Decorate an async function to register it as a health check.
Registration is a side effect: the function is returned unchanged
with its original signature, so await-ing it directly (for
example in a unit test) type-checks as usual.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
Unique name identifying this check.
TYPE:
|
critical
|
Whether this check affects the aggregate status. For a liveness check, whether its failures can stop the worker.
TYPE:
|
timeout
|
Per-check timeout override.
TYPE:
|
liveness
|
Run this check for
TYPE:
|
Example
@health.check("database") ... async def check_db() -> dict | None: ... return None
add_provider
add_provider(
provider: Provider,
*,
name: str | None = None,
critical: bool = True,
timeout: PositiveFloat | None = None,
) -> None
Register a provider's built-in readiness check as provider:{name}.
| PARAMETER | DESCRIPTION |
|---|---|
provider
|
The provider whose built-in readiness check to register.
TYPE:
|
name
|
Check name suffix. Defaults to the provider's
TYPE:
|
critical
|
Whether the check affects
TYPE:
|
timeout
|
Per-check timeout override. Falls back to the default.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the provider ships no readiness check, the
resulting name is already registered, or |
add_circuitbreaker
add_circuitbreaker(
breaker: CircuitBreaker,
*,
name: str | None = None,
critical: bool = False,
timeout: PositiveFloat | None = None,
) -> None
Register a check on a circuit breaker as circuitbreaker:{name}.
Each run reads the breaker's state from its backend, so a
replica that made no call through a shared breaker still reports
it open. A closed breaker is ok. A half-open or forced-closed
breaker is ok with its state in the details. An open breaker
fails with the seconds until its next probe, and a forced-open
one fails until it is reset.
| PARAMETER | DESCRIPTION |
|---|---|
breaker
|
The circuit breaker to report. A per-key circuit from
TYPE:
|
name
|
Check name suffix. Defaults to the breaker's
TYPE:
|
critical
|
Whether an open breaker fails
TYPE:
|
timeout
|
Per-check timeout override. Falls back to the default.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the resulting name is not a valid check name
(pass |
run
async
run(
*,
critical_only: bool = False,
exclude: Iterable[str] | None = None,
) -> HealthReport
Run the selected checks concurrently and aggregate.
Each check runs with its own timeout. Results are cached per
check for cache_ttl. Concurrent calls for the
same check coalesce via single-flight.
| PARAMETER | DESCRIPTION |
|---|---|
critical_only
|
If True, only run critical checks.
TYPE:
|
exclude
|
Check names to skip.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
HealthReport
|
A HealthReport with the aggregate status and per-check |
HealthReport
|
results. |
HealthChecksConfig
Bases: BaseModel
Health Checks Config.
timeout
class-attribute
instance-attribute
timeout: float = 5.0
Default per-check timeout in seconds. Checks that exceed this duration are reported as error. Can be overridden per check on registration.
cache_ttl
class-attribute
instance-attribute
cache_ttl: Retention = timedelta(seconds=1)
Per-check cache TTL, in whole seconds or as a timedelta. A float is refused. Each check's last result is reused until it is cache_ttl old. Concurrent calls coalesce via single-flight. Set to 0 to disable caching. From text, such as an environment variable, it reads whole seconds ("60") or an ISO 8601 duration ("PT0.5S").
HealthReport
Bases: TypedDict
Aggregated health report across all registered checks.
HealthStatus
Bases: StrEnum
Binary health status for a component or aggregate report.
OK: the check passed. At the aggregate level: every critical check passed (non-critical failures do not flip the aggregate).ERROR: the check failed. At the aggregate level: at least one critical check failed.
OK
class-attribute
instance-attribute
OK = 'ok'
ERROR
class-attribute
instance-attribute
ERROR = 'error'
Liveness
Bases: BaseModel
When a worker counts as stuck, and exits so it is replaced.
A stuck worker exits, and the process manager that runs it starts
another: the uvicorn supervisor, the Gunicorn arbiter, or Kubernetes for
a container whose only process ends. A loop that stalls past
stall_timeout exits at once. Liveness checks that fail
failure_threshold rounds in a row stop the worker with SIGTERM, and
/livez answers 503 from the first failed round.
stall_timeout
class-attribute
instance-attribute
stall_timeout: float | None = None
Seconds the event loop may go without running a callback. A watchdog thread checks four times per stall_timeout, at most once a second. Past it, the watchdog logs the loop thread's stack and exits the process. None (the default) starts no watchdog.
interval
class-attribute
instance-attribute
interval: float = 10.0
Seconds between two runs of the liveness checks.
failure_threshold
class-attribute
instance-attribute
failure_threshold: PositiveInt = 3
Liveness rounds that must fail in a row before the worker stops itself with SIGTERM. /livez answers 503 from the first one.
shutdown_timeout
class-attribute
instance-attribute
shutdown_timeout: float = 30.0
Seconds a worker stopped by failed liveness checks has to shut down. Past it, the worker exits at once, as it does when it runs as PID 1 with no SIGTERM handler or its shutdown hangs.
health_asgi
health_asgi(
component: HealthChecks | None = None,
*,
prefix: str = "",
show_details: bool = False,
) -> ASGIApp
Create a pure-ASGI app serving the health endpoints.
The three endpoints
health_router serves,
rendered by the same code, with no framework anywhere:
GET/HEAD {prefix}/livez: Liveness probe. Runs no check per request.503while a liveness check fails, else200, with an empty body.GET/HEAD {prefix}/readyz: Readiness probe. Runs critical checks only.200or503with an empty body.GET/HEAD {prefix}/healthz: Aggregate JSON report.
Both probe paths accept ?exclude=name,name, and every response sets
Cache-Control: no-store. Any other path answers 404, and any
other method 405.
Mount it in an ASGI framework:
from starlette.applications import Starlette
from starlette.routing import Mount
from grelmicro.health import health_asgi
app = Starlette(routes=[Mount("", app=health_asgi())])
Or give it a port of its own with
OpsServer, for a worker that runs no web
framework at all.
On FastAPI, prefer health_router(): it serves the same endpoints and
adds the OpenAPI schema, the Depends gate on /healthz, and the
dependency form of show_details.
| PARAMETER | DESCRIPTION |
|---|---|
component
|
Health checks component whose checks the app runs. When omitted, it resolves the default instance from the active
TYPE:
|
prefix
|
URL prefix for the three paths. Leave it empty when the app is mounted, because the mount strips its own path first.
TYPE:
|
show_details
|
Whether
TYPE:
|