# Monitoring thresholds: checked and generated

How `jadpo check` reads a domain's thresholds and what it writes from
them (chunk B13; the form of a threshold decided 2026-11-01).

## Where they are read

A domain has one `thresholds { }`, in its `monitoring.jadpo`; a second,
or one in any other file of a project, is
`threshold.declaration_invalid`. A loose file may hold one, watching
what the loose files declare. A threshold's name is unique in its
domain, and its subject, as an acceptance names it, is
`<domain>.<threshold>`.

## In the deployable

The per-request limits, `duration:` and `queries:`, go into the
runtime's plan with the identity key of what each watches, so both
executors and every deployable hold the same thresholds. They are what
the `telemetry:` section judges each request by once it is built; until
then nothing reads them.

## The rules in `build/monitoring/`

`jadpo check` writes two files once the thresholds read cleanly, and
none while there are no across-request limits or floors:

- **`prometheus.yaml`**, Prometheus alerting rules: a group for each
  domain, `jadpo.<domain>`, and `jadpo.files` for the disk floors, with
  an alert for each across-request limit, named
  `<domain>_<threshold>_<limit>` and labelled `jadpo_threshold` with its
  subject, its `///` line as the summary.
- **`openslo.yaml`**, OpenSLO v1: a `Service` for each domain and an
  `SLO` for each percentile and `server_failures:`, each counted by
  `Occurrences` over a rolling `within:`. A `p95: 300ms` is the share of
  requests done within 300 ms, with a target of `0.95`; a
  `server_failures: 1%` the share that didn't fail on the server, with a
  target of `0.99`. `oldest_waiting:` and the floors are conditions
  rather than objectives, so they are Prometheus rules only.

The rules read the metrics the runtime will send, under the names a
Prometheus that takes OpenTelemetry gives them:

| Metric | Prometheus name | Attributes |
|---|---|---|
| A route's duration, a histogram in seconds | `http_server_request_duration_seconds` | `http.request.method`, `http.route` as `jadpo routes` prints it, `http.response.status_code` |
| An action's, query's, schedule run's or delivery's duration, a histogram in seconds | `jadpo_run_duration_seconds` | `jadpo.kind` (`action`, `query`, `schedule`, `subscriber`), `jadpo.name` (`hello.greet`), `jadpo.outcome` (`ok`, `failure`, `server_failure`) |
| The age of a subscriber's oldest waiting delivery, a gauge in seconds | `jadpo_delivery_oldest_waiting_seconds` | `jadpo.name` |
| The share of a disk store's disk that is free, a gauge from 0 to 1 | `jadpo_files_free_ratio` | `jadpo.connection` |

Each duration histogram has a bucket boundary at every duration a
threshold on it names, so a percentile's count is exact rather than
read between buckets.

## In the manifest

Each threshold is a `threshold` node named by its subject, with a
`uses` edge to what it watches, `watches` saying what kind of thing that
is, and its limits as written, its per-request ones under `perRequest`
and the others under `acrossRequests` beside `within`. A disk store's
floor is a `threshold` node named `<connection>.free_space_floor`, which
watches `disk` and uses its connection.
