On this page
Public status
Whether the components people rely on work, as the fleet sees them from outside. A product declares its components in the catalog; Stado asks each one and publishes what it answered; a status page reads what Stado published. Nothing else keeps a list of components.
Declaring a component
Each catalogued product may declare public_status in
catalog/products.yml: one entry per component a person uses, with the one
request that shows it works for them.
public_status:
- component: front-page
title: Front page
method: GET
url: https://app.example.com/
expect_status: OK
- component: guest-sign-in
title: Guest sign-in
method: GET
url_secret: example-database#url
path: /auth/v1/settings
headers:
apikey: example-database#anon_key
expect_status: OK
expect_body: '"anonymous_users":true'
- component: chat-api
title: Chat API
method: POST
url: https://api.example.com/api/chat
body: {}
expect_status: Unauthorized
expect_body: Missing Authorization header
- component: messaging
title: Messaging
method: GET
service: example-messaging
path: /healthz
expect_status: OK
| field | meaning |
|---|---|
component |
a slug, unique within the product |
title |
the name a person reads on a status page |
method |
the HTTP method, in capitals |
url |
an absolute HTTPS address, path included |
url_secret |
role#field of a vault item whose field is the base address; path follows it |
service |
a service directory key; the address is this host's forward marker (~/.stado/forwards/<service>.local, written by stado service directory publish); path follows it |
headers |
header name to role#field: header values only ever come from the vault, and are never printed |
body |
a JSON body to send |
expect_status |
the status a working component answers, by its HTTP reason phrase (OK, Unauthorized) |
expect_body |
optional text the answer of a working component carries |
Exactly one of url, url_secret and service is declared. The catalog
refuses anything else when it is validated (stado product catalog):
<product>.public_status.<component>: declare exactly one of url, url_secret and service, …,
… is not a vault reference role#field, … url must be an absolute HTTPS URL,
… method "get" is not an HTTP method in capitals, duplicate component ….
A request that is refused on purpose is a good check: a chat route that
answers Unauthorized to a request without a session is up, and it creates
nothing.
Checking and reading
stado public-status check [--json]
stado public-status show [--json]
check sends every declared request once, from the machine it runs on, and
publishes the outcome in the fleet's object store
(public-status/components.json). Each component's row carries:
| field | meaning |
|---|---|
product, component, title |
what was checked |
state |
up when the answer was the declared status (and text), down otherwise |
since |
when the component entered that state: kept from the previous check while it does not change |
checked_at, detail |
this check, and the answer that decided it |
last_failure |
{at, error} of the newest failed check, kept after the component recovers; null when none was recorded |
A down component's detail is the answer itself: the status and the whole
body the component sent (GET https://… answered 502 Bad Gateway where a working component answers OK: …), the transport's own words when nothing came
back (… got no answer: …), or why the request could not be made — a vault
field Skarbiec did not give, a forward marker this host does not hold. The
document also names checked_at and checked_by (the machine that checked).
show prints the published document: with --json as it is stored, without
it one line per component, Front page (wisent-app/front-page): up since …; last failed at …: …. When nothing is published it exits unsuccessfully with
no status is published at public-status/components.json in the object store: no stado public-status check has run against it; a stored document that is not
JSON is named the same way.
Whatever runs check is its cadence. Run it from a host that is not the one
serving the components, for example:
stado schedule create --cron '* * * * *' 'stado public-status check'
The status page
stado public-status serve --bind <host:port>
serve is the page people read. Every request reads the published document
again, so the page says what the last check found:
| request | answer |
|---|---|
GET / |
the page: each component's title, state, since when and its last failed check with the answer that failed it, and when and from which machine the last check ran |
GET /status.json |
the document exactly as check published it |
| either, when the document cannot be read | 503 Service Unavailable with the reason in the store's own words (the page) or {"error": "…"} (the document), never an old or empty page |
| another path | 404 Not Found |
another method than GET or HEAD |
405 Method Not Allowed |
The listener reads every request with Stado's API listener's own reader, so it
bounds the entire request head, including its terminating blank line, by
dashboard.request_limits.head_bytes (or the measured available host memory
when no request limits are declared), and refuses a malformed request the same
way, with the same statuses and sentences, before the store is read: a head
past the bound answers 431 Request Header Fields Too Large naming the bound;
a request line that is not <method> <target> HTTP/<version>, a header line
without a colon, a head that is not UTF-8 or a client that closes before the
head ends answers 400 Bad Request quoting what was wrong; a
Transfer-Encoding answers 501 Not Implemented. The full list is on
API request boundaries. A complete head
ending exactly at the bound is accepted, and bytes buffered after the
terminator do not count against the head. A connection that fails on the
socket itself is not answered; standard error names the client and the
cause. An unreadable limit declaration refuses startup before the listener
binds, naming the declaration and its cause.
Its first line on standard output says where it listens:
stado public-status: the status page listens on <address>; an address it
cannot bind is refused as cannot listen on <bind>: <error>.
The page must answer while the products it reports on do not, so it runs on a
host apart from them, as a role of that host's one Stado process: stado serve --public-status-bind <loopback host:port> serves the same page inside the
process, under the role name public-status (stado public-status serve --bind is the same page run from a terminal). Its hostname is published on
its own domain, on the edge in front of that host, as a web declaration in
front of the service directory entry that names that loopback address:
stado web declare public-status --hostname <status-hostname> --upstream-service public-status
stado web route public-status
Real test
tests/public_status/ (cargo test --manifest-path stado-rs/Cargo.toml --test public-status, with STADO_SOURCE_REVISION naming the committed candidate)
runs the built Stado on an isolated object store against the real endpoints the
compiled catalog declares. check.rs: show is refused before any check,
check publishes one row per declared component with its cause, a second check
keeps since for every unchanged state, and show answers what the last check
published. serve.rs: the page answers 503 naming why before anything is
published, then 200 with every declared component and /status.json equal to
show --json, 404 for another path and 405 for a POST. A real socket request
whose head ends at the declared bound is accepted and an oversized head
receives 431 naming the bound; a short request line, a header line without a
colon and a head that is not UTF-8 each receive 400 quoting the fault, and a
Transfer-Encoding receives 501. The report retains every response under the
exact source revision.
Source: this website