SLO + business-impact engine
What this is
An SLO (service-level objective) is a promise about reliability, written as a number: "checkout should succeed 99% of the time." The thing you actually measure to check the promise — the ratio of good probes to total probes — is the SLI (service-level indicator). This engine turns that promise into something probectl watches continuously, and translates it into the language an executive cares about: are we keeping the promise, how much room do we have left before we break it, and is something burning that room down right now?
It sits in the control plane (internal/slo). It reads the same synthetic
probe results probectl already collects, evaluates each SLO per tenant, and
raises an alert into the incident pipeline when reliability is being spent too
fast. Definitions are written in OpenSLO (an open, vendor-neutral SLO
format), so they move freely between probectl and any other OpenSLO tooling.
Four pieces fit together:
- SLI/SLO definitions — what you measure (the good/total ratio) and the target you hold it to.
- Error budgets — the small amount of failure the target allows, treated as a spendable balance.
- Multi-window, multi-burn-rate alerts — the Google SRE method (SRE — site reliability engineering, Google's discipline for running services) for paging on real problems without paging on noise.
- Service/team mapping — every SLO carries a business owner, so reliability rolls up to a team, not just a probe target.
OpenSLO conformance — conform, do not diverge
A definition is a standard OpenSLO v1 document (apiVersion: openslo/v1,
kind: SLO). probectl evaluates a deliberate subset of OpenSLO, and is strict
about it:
ratioMetricindicators (agoodcount over atotalcount) wheremetricSource.typeisprobectlbudgetingMethod: Occurrences(count successes vs. attempts — not a time-slice method)- exactly one rolling
timeWindow(30d,7d,12h, … — rolling means the window always ends now and slides forward, never a fixed calendar month) - exactly one objective
target, a ratio strictly between 0 and 1
Anything outside that subset is rejected loudly when the file loads — the YAML parser runs in strict-fields mode, so an unknown or unsupported field stops startup rather than being silently ignored. Export emits the original document back unchanged (a lossless round-trip, enforced by a test). The reasoning: an SLO you think probectl is tracking must actually be tracked. Silently dropping one is worse than refusing to boot.
apiVersion: openslo/v1
kind: SLO
metadata:
name: checkout-availability
displayName: Checkout availability
labels:
team: payments # the business-unit mapping (showback)
tenant: 88929fbe-28e5-4f0a-898e-6c4302c55e57 # the tenant this SLO belongs to (id)
spec:
service: checkout
indicator:
metadata: { name: checkout-probe-success }
spec:
ratioMetric:
good:
metricSource:
type: probectl
spec: { canary_type: http, target: checkout.acme.example, outcome: success }
total:
metricSource:
type: probectl
spec: { canary_type: http, target: checkout.acme.example }
timeWindow: [{ duration: 30d, isRolling: true }]
budgetingMethod: Occurrences
objectives: [{ target: 0.99 }]
A few matching rules worth knowing:
target(the probe target) accepts a trailing*as a prefix wildcard (api.*matchesapi.acme.example,api-internal.example, …).metadata.labels.tenantbinds the definition to one tenant (its id): only that tenant's results feed it and only that tenant lists or exports it. In the multi-tenant and regulated profiles every definition must carry it — loading refuses one that does not — because a deployment-wide definition would feed on every tenant's results and appear, with its service, team, target and test id, in every tenant's/v1/slos(DPR-068). Single-tenant deployments may omit it.canary_typeleft empty matches any probe type.targetis the probe'sserver.address— for HTTP canaries that is the host (checkout.acme.example), not the URL, so two HTTP definitions against one host (/and/checkout) are indistinguishable by target. Name the server test definition instead:test_id: <id>(the id the test receipt returns; results carry it asprobectl.test.id) selects exactly that definition, with or without atarget(DPR-066).- The
goodmetric must declareoutcome: success;goodandtotalmust share the sametarget/test_id(at least one non-empty) and the samecanary_type— otherwise the ratio would compare two different things.
Definitions load from the directory named by PROBECTL_SLO_DIR (each file may
hold multiple YAML documents separated by ---). A malformed file, an invalid
duration, two SLOs with the same name, or a definition file larger than 1 MiB
fails startup. The size check happens before YAML decoding.
Error budgets + multi-window burn-rate alerts
Start with the intuition. If your target is 99%, you are allowed to fail 1% of the time. That 1% is your error budget — a balance you spend as failures happen. Burn rate is how fast you are spending it:
burn rate = errorRate(window) / (1 − target)
The budget behaves exactly like a monthly mobile-data allowance: burn rate 1 is the pace at which you use precisely your whole allowance by the end of the month — fine, by definition. At burn rate 1 you spend exactly the whole budget over the SLO window and land on empty right at the end — sustainable. At burn rate 14.4, you spend the entire month's budget in about two days — an emergency.
The hard part of alerting is telling a real outage apart from a blip. probectl uses the Google SRE answer: require two windows — a long one and a short one — to both exceed the threshold before it fires (a logical AND). The long window proves the problem is sustained (kills noisy, flappy alerts); the short window proves it is still happening right now (kills slow, stale alerts that fire long after recovery).
| Tier | Long window | Short window | Burn ≥ | Severity |
|---|---|---|---|---|
| fast | 1h | 5m | 14.4 | critical (page) |
| medium | 6h | 30m | 6 | critical (page) |
| slow | 3d | 6h | 1 | warning (ticket) |
(A long window is clamped to the SLO window itself — for a 12h SLO the slow
tier's 3-day window evaluates over 12 hours, since no window can be longer than
the budget it measures.)
Worked example, against the 99% checkout SLO above (so 1 − target = 0.01). Say
over the last hour 14.4% of probes failed: 0.144 / 0.01 = burn rate 14.4. If
the last 5 minutes are also failing at ~14.4% or worse, both the fast long
and fast short windows clear 14.4, and the engine pages. If the 5-minute window
has already recovered, nothing fires — the incident is over.
When a tier first crosses, the engine raises a slo.burn_rate signal (plane
slo) into the incident pipeline. Signals are latched per window per
episode — latched meaning the signal fires once and then stays armed-off: one
signal when a tier starts firing, and it re-arms only after the long window
drops back under the threshold. Clearing on the long window (not the short one)
is deliberate hysteresis — a gap between the turn-on and turn-off
conditions, the way a thermostat that heats to 21° refuses to restart until
the room falls below 19° — and it stops a single episode from flapping out a
stream of alerts on short-window jitter.
Cold start — an empty baseline is not an outage
A brand-new SLO, or one whose probes barely run, has almost no data. A single
failure out of three probes is a 33% error rate, which would trivially trip
every burn threshold — a false alarm. So the engine stays quiet until an SLO has
seen at least 50 events in its full window, and reports cold_start: true
until then.
The threshold is checked against the full SLO window, not against each alert window. That distinction matters: a low-cadence probe (say one every few minutes) might never accumulate 50 events inside the 1-hour fast window, so gating on the alert window would make fast alerts permanently dead. Gating on the full window instead lets slow probes still get fast alerts once they have enough history overall.
Surfaces
GET /v1/slos(permissionmetrics.read) — the caller's tenant's SLO statuses: attainment, error budget remaining, total events, the cold-start flag, and per-window burn rates with their firing state.slo_running: falsemeans the engine is not wired in.GET /v1/slos/openslo(permissionmetrics.read) — the loaded definitions as an OpenSLO v1 YAML stream. Definitions are deployment-level configuration; statuses are per tenant, so this endpoint returns the shared definitions and/v1/slosreturns the tenant-scoped numbers.- SLOs page (
/slos) — the executive dashboard: attainment vs. objective, an error-budget bar, burn-rate badges, service/team labels, and honest cold-start and not-wired states. - What-if integration — the topology what-if (a read-only simulation that
fails an element in a copy of the graph) reports
impacted_slos, the SLOs whose service or probe target sits inside the simulated blast radius, so "what breaks if this link dies?" answers in SLO terms.
%%{init: {'theme':'base','themeVariables':{'background':'#0d1117','primaryColor':'#161b22','primaryTextColor':'#e6edf3','primaryBorderColor':'#3b82f6','lineColor':'#8b949e','secondaryColor':'#21262d','tertiaryColor':'#0d1117','clusterBkg':'#161b22','clusterBorder':'#30363d','fontFamily':'ui-monospace, SFMono-Regular, Menlo, monospace'},'flowchart':{'curve':'basis','nodeSpacing':55,'rankSpacing':55,'padding':12}}}%%
flowchart LR
R[synthetic results] --> C[SLOConsumer]
C --> E[slo.Engine<br/>per-tenant minute buckets]
D[OpenSLO YAML<br/>PROBECTL_SLO_DIR] -->|strict load| E
E -->|statuses| API[GET /v1/slos]
E -->|OpenSLO out| EXP[GET /v1/slos/openslo]
E -->|"slo.burn_rate signals"| INC[incident pipeline]
E -->|impacted SLOs| WI[what-if simulation]
API --> UI[SLOs page]
Under the hood the engine keeps per-tenant, per-minute buckets of good/total
counts, pruned to the SLO window. Burn over any window is computed by summing
the buckets in that window — so the same data answers both the live status and
the alert evaluation.
Configuration
| Variable | Default | Purpose |
|---|---|---|
PROBECTL_SLO_ENABLED |
true |
the engine + result consumer (local-only) |
PROBECTL_SLO_DIR |
(none) | directory of OpenSLO YAML definitions; each file is limited to 1 MiB; empty means zero SLOs, honestly reported |
On Kubernetes the definitions are a ConfigMap (one key per OpenSLO file) named
in control.slo.existingConfigMap; the chart mounts it read-only at
control.slo.mountPath (/etc/probectl/slo) and sets PROBECTL_SLO_DIR for
you (DPR-064) — nothing else in the chart could carry an operator file to the
control plane, so the engine was unreachable on that deploy path.
kubectl -n probectl create configmap probectl-slo --from-file=slos.yaml
helm upgrade probectl deploy/helm/probectl --reuse-values \
--set control.slo.existingConfigMap=probectl-slo
Out of scope by design: application-level SLOs. probectl correlates the network planes; it does not own application instrumentation.