Discovery
#[indicators] submits each tagged method to a link-time registry.
At probe time, HealthService drains that registry and filters it
against ReachableProviders — the access-graph-derived set of every
provider reachable from the running app’s root module. The same
module-gating every transport uses for #[controller], #[resolver],
or #[processor] applies here.
The rule
Section titled “The rule”| Provider state | Indicator behavior |
|---|---|
Provider lives in a module imported by AppModule (directly or transitively) | Runs on every matching probe |
| Provider linked into the binary but in no reachable module | Silently skipped; the skip is logged at debug on nest_rs::health |
Linking a crate without importing its module keeps every provider in
that crate inert — present in the binary, not mounted on any
transport. Health indicators inherit that contract: a worker app
that imports UsersModule but not UsersHealthModule does not run
the users indicator, even though the symbol is in the binary.
Why this matters
Section titled “Why this matters”Two reasons.
Per-app subsets. crates/features/ ships every adapter a feature
can have — HTTP controller, GraphQL resolver, queue processor, health
indicator. An app picks which ones it serves by importing the
matching modules. The worker binary stays free of HTTP surface even
though it links the feature crate. Health is no different: the
indicator only runs where the app asked for it.
No surprise checks. An indicator is a check, and a check on the
wrong app is worse than no check. A startup probe that pings a
database an MCP-only worker doesn’t even own would drop the worker’s
/health/startup to 503 forever. Module-gating keeps the check
where its provider belongs.
Checking what runs
Section titled “Checking what runs”When an indicator silently sits out, the framework emits one line
per skipped check on the nest_rs::health target:
DEBUG nest_rs::health: skipped indicator: provider unreachable from app's module tree indicator=upstream kind=ReadinessRun with RUST_LOG=nest_rs::health=debug to surface them. The usual
cause is a missing <Feature>HealthModule import in AppModule —
the feature’s data module imports the service, not the health adapter.
A second check: the response body. With the indicator’s module
imported, the indicator’s name appears under details on its probe:
$ curl -s http://localhost:8080/health/ready | jq '.details | keys'[ "db", "upstream" ]Missing key, missing import. Present key with status: "down",
genuine failure — read the error bucket.
Going further
Section titled “Going further”- Indicators — where they live and how to write one.
- Providers — the access graph, the reachable set, and what makes a provider reachable.
- Modules —
imports = [...], idempotent registration, the port + adapters layout.