Health probes: /health and /ready
Core ships both. Do not write your own — every app repo that did so ended up
maintaining a near-identical copy, and the reason they did was that the only
probe used to be /ready, which was not safe to point a monitor at.
Both are mounted by core with no authentication: a monitor cannot log in.
/health |
/ready |
|
|---|---|---|
| Question | Is the process serving? | Can it do useful work? |
| Checks | nothing | initialised, plus the database answers |
| Degraded app | 200 | 503 |
| Still starting | 200 | 503 |
| Body | {"status": "ok"}, plus "build" when SOURCE_COMMIT_APP is set |
{"status": "ready"} / {"status": "not_ready", "reason": …} |
| Point it at | container liveness, uptime monitors | load balancers, readiness gates, deploy checks |
Why two
They answer different questions, and one endpoint cannot answer both.
An orchestrator restarts a container whose liveness probe fails. So liveness must fail only when a restart is the right response. A degraded FastPluggy app — one that booted without its database — is live: the process is serving, and the degraded-mode reprobe may heal it seconds later without help. Report that as "dead" and the orchestrator kills it on every check, turning a recoverable outage into a crash loop.
A load balancer, meanwhile, needs the opposite: stop sending traffic the moment
the app cannot serve a request, and resume when it can. That is /ready.
This is why /health takes no dependency at all — not the database, not the
plugin registry, not even the FastPluggy instance. Anything it touches is
something that can fail and trigger a pointless restart.
What they must never return
Both are unauthenticated, so neither returns plugin names, counts, versions,
settings, or any connection detail — with one deliberate exception, the build
identifier below. /ready's reason is a fixed word from a
closed set (starting, database) and never the driver's error message —
connection errors routinely carry the host, the database name, and sometimes
credentials.
If you need richer diagnostics, put them behind authentication. An open endpoint is the wrong place to answer "which database am I connected to".
Availability during a failed boot
/health is registered in FastPluggy.__init__, before the database is first
probed, and the degraded-mode middleware exempts it. That is deliberate: a boot
that fails never reaches load_app(), so a probe registered there would return
404 in exactly the situation the orchestrator is asking about — and a 404 is
as much a failure as a 503.
/ready no longer triggers a reload
It used to call trigger_reload(plugins_dir), which unlinks a marker file in
the plugins directory on every request. Under --reload that unlink is a
filesystem event, so polling readiness restarted the app; without --reload
it was a stray unlink on every poll. A probe reports state, it does not change
it. trigger_reload is unchanged and still available to the callers whose job
is actually reloading.
If you were relying on GET /ready as a way to force a reload in development,
call trigger_reload directly or restart the dev server.
The one admitted field: build (core >= 0.4.65)
/health echoes a build identifier when the env var SOURCE_COMMIT_APP is
set, and is silent when it is not. Set nothing and the body is byte-identical to
what it always was.
{"status": "ok", "build": "a1b2c3d"}
It is admitted where versions are refused because it is an identity, not a
version: a commit SHA of a private repo does not tell an unauthenticated caller
which release you run or which advisories apply to it, the way "FastPluggy
0.4.60" would. It answers the one question a bare {"status": "ok"} cannot —
is what I just deployed actually running? — which otherwise costs a registry
digest comparison and still leaves doubt.
The name is not a preference. Nine app repos already set SOURCE_COMMIT_APP, so
core adopting a different one would have left every one of them rolling its own
probe. build_identifier() reads the environment per call, never cached at
import: a value captured before the environment was assembled would outlive the
truth.
The same identity as a metric: fp_core_build_info
core_metrics.py emits the standard *_build_info shape — a constant 1 whose
labels carry the information — so the running build can be joined onto any
other series ("alert when the running build != the expected one") in a way a JSON
body never can. Emitted only when the variable is set, same rule as the probe.
⚠ The metrics router auto-prefixes every core metric name with fp_core_. The
entry is registered as build_info and scrapes as fp_core_build_info — not
fastpluggy_build_info, which is the name issue #29 proposed before the prefixing
convention was checked. Never start a name in core_metrics.py with fp_, or it
scrapes as fp_core_fp_….