Discovery
How module-gating decides which indicators run in a given binary — and what to check when one goes silently inert.
#[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 | Skipped; named once at boot with a warn on nest_rs::health, so it shows at any production filter |
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 names it once at
boot on the nest_rs::health target — the same contract every other
discovery seam honours (nest_rs::queue, nest_rs::events):
WARN nest_rs::health: skipped indicator: no instance of the provider in this app's container indicator="upstream" kind=Readiness origin="features::upstream::health" hint="nothing is registered under that exact type. Common causes: …"warn for your indicator, so it shows at any production filter, and once —
a wiring notice repeated on every probe is log volume, not a signal. An
indicator a nest-rs-* crate ships and this app never opted into reports at
debug instead: it is not your mistake and you cannot act on it, so a DB-less
app is not told twice per boot to go bind nest_rs_seaorm’s. Probing then skips in
silence. 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:3000/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.