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