Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferenceMonitoring

Built-in services

Monitoring

partly builtMissing: telemetry, recording the path each request takes, and jadpo traces, metrics, logs and path. See What is built.

On this page

A threshold says when something your app does is too slow, too frequent or failing too often, such as a route's p95 duration. You declare thresholds in your app's source, in a monitoring file for each domain, so they are reviewed with the code and versioned with each build rather than configured in a monitoring tool.

jadpo check holds each threshold to what it watches and writes rules for your monitoring tool. Nothing sends those metrics yet, since telemetry: isn't built (Telemetry), so a threshold today is checked, reviewed and turned into rules, and doesn't yet raise an alert by itself.

Telemetry not built

The generated runtime reports what it does as OpenTelemetry traces, metrics and logs, the open standard most monitoring tools accept.

Path IDs not built

Every request records the path it took through your app: the route, each action and query, and each branch taken inside them.

Writing a threshold

A domain's thresholds are its thresholds { }, in domains/<domain>/monitoring.jadpo. Each is name: { } giving one thing it watches and its limits, and its /// line, what it protects, is the summary of the alerts made from it. Here one threshold holds an action's p99 to 50 ms.

This builds on a project from jadpo new, which has a hello domain with an action called greet and a route, POST /hello, that runs it. Add this one file. thresholds { }, watches: and p99: are built into Jadpo. greeting is your name for the threshold, and the use line imports jadpo new's greet.

domains/hello/monitoring.jadpo:

example
use hello/actions { greet }thresholds {    /// Working out a greeting stays quick.    greeting: {        watches: greet        p99: 50ms    }}

A threshold can watch a route instead, and hold it to several limits at once. This one watches the POST /hello route.

domains/hello/monitoring.jadpo, with greeting_route new:

example
use hello/actions { greet }thresholds {    /// A visitor is greeted quickly.    greeting_route: {        route: {            method: HttpMethod.post            path: "/hello"        }        duration: 800ms        queries: 2        p95: 300ms        server_failures: 1%        within: 5m    }    /// Working out a greeting stays quick.    greeting: {        watches: greet        p99: 50ms    }}

What it watches is in its own domain:

  • route: names a route by its method: and its path as jadpo routes prints it, placeholders by name, as a route test names one;
  • watches: names an action, query, schedule or subscriber, imported like any name the file uses. Their names are unique across the project, so the name says which.

Its limits, each an upper bound:

Limit Kind Watches Value
duration: Per request Anything The longest one request or run may take; a route's is shorter than its deadline:
queries: Per request All but a query The most queries one may run
p50:, p90:, p95:, p99: Across requests Anything That share of the requests within within: takes at most this long
server_failures: Across requests Anything The share within within: that may end in a fault or a failure of a kind answered with a 5xx: unavailable, timed_out, outcome_unknown, misconfigured or internal_fault; never one a client caused
oldest_waiting: Across requests A subscriber The age its oldest waiting delivery may reach

within: is the window the across-request limits are judged over, a whole number of minutes, hours or a day from 1m to 1d, and 5m when left out. A percentile is no shorter than a lower one beside it, and server_failures: is above 0% and below 100%. A threshold that names nothing in its domain is threshold.subject_unknown; any other mistake in one, such as a limit that doesn't apply to what it watches, is threshold.declaration_invalid.

Thresholds per request

These judge one request: its duration and its number of queries. They are checked and kept in the runtime's plan, and nothing reads them yet. They come into use with telemetry:, when the runtime will keep each request's spans in memory until the request ends, then send:

  • the full trace when a threshold was crossed, the request failed or faulted, or its path is new to this process;
  • otherwise metrics only, plus one sample trace for each path each minute, so there is always a normal request to compare a bad one with.

telemetry: { detail: } sets both (Telemetry): full_traces: names the first list and samples: the second.

A process that crashes loses the requests it hasn't sent yet, and long jobs send on a timeout. This keeps the volume low without losing anything structural: the compiler knows every path, so the number of per-path metrics can't grow without bound.

Thresholds across requests

These judge many requests together: a p95 over a window, an error rate, the oldest waiting delivery. One instance can't judge them for a fleet, so jadpo check writes rules for your monitoring tool into build/monitoring/: prometheus.yaml and openslo.yaml. The build manifest carries the same thresholds, so a tool that reads it needs no generated rules. You load the files into your tool yourself. They are generated, so a change belongs in the thresholds, not in the file.

The p99: above becomes this rule in build/monitoring/prometheus.yaml:

yaml
- alert: hello_greeting_p99  expr: "histogram_quantile(0.99, sum by (le) (rate(jadpo_run_duration_seconds_bucket{jadpo_kind=\"action\", jadpo_name=\"hello.greet\"}[5m]))) > 0.05"  labels:    jadpo_threshold: "hello.greeting.p99"  annotations:    summary: "Working out a greeting stays quick."    description: "hello.greet p99 above 50ms within 5m"

A disk store's free_space_floor: (The disk driver) is one of these too: the rules raise an alert when the store's disk has less free than its floor, as puts start answering storage_full.

Setting and changing them

  • Loosening needs a person. Raising or removing a threshold is how an agent would silence an alert, so it needs an acceptance and appears in the approval report. Tightening one doesn't. jadpo check compares each threshold with the one of the same name on the before side, read from git: a limit raised, or a longer within:, is planning.todo_list.p95: ThresholdChange.raised; a limit gone is ThresholdChange.removed on the limit, and a threshold gone, or watching something else, on the threshold, planning.todo_list; a disk store's floor lowered is uploads.free_space_floor: ThresholdChange.lowered. A new threshold, a lower limit and a higher floor need nothing.
  • Per build. Thresholds belong to a build.

Not built yet: starting values worked out from the query report and from production, an agent proposing changes from what it observes through the MCP server (The developer console and MCP), and metrics for each flag value's path (Path IDs).

Details

The exact rules, for when you hit an edge case.

What Jadpo generates from thresholds

jadpo check reads each domain's thresholds and writes rules a monitoring tool can load.

Markdown for agents: /docs/monitoring-thresholds.md.Built from Jadpo main at aee2e8f.