Observability · the obs tool
One tool, obs, runs every check
Cheap checks run on every pull request in seconds with no containers. Data checks run only when observability/ changes, on a small local stack. Two checks only comment: the dashboard look and the AI review.
- obs is a tool in
AtomiCloud/, not a graph node. DECIDED · A193 tab 1 #17nix-registry - None of obs is built yet. Every message below is the planned wording. DECIDED · A193 tab 2 NOT TESTED · not built
Plant a mistake
Pick a mistake. See which check catches it, when, and the exact message.
What catches it
blocks Step 1 · contract → code
obs gen --check
When: commit + every PR, ~1 s
signals.g.ts is stale vs contract.yaml (sha 9f2c… ≠ 41ab…); run obs genDECIDED · A193 tab 2
blocks Step 1 · contract → code
obs config --check
When: commit + every PR, ~1 s
raichu.yaml: otel.traces.sampler.ratio 0.001 < contract min_ratio 0.01DECIDED · A193 tab 2
blocks Step 1 · contract → code
the compiler
When: as you type
TS2345: Property 'event' is missingDECIDED · A193 tab 1 #1; tab 3
blocks Step 2 · code uses ports
lint rule (Biome here)
When: commit + every PR
src/lib/charge.ts:1 noRestrictedImports: import from '@opentelemetry/api' — use the Telemetry portsDECIDED · A193 tab 2
blocks Step 2 · code uses ports
forbidigo lint rule
When: commit + every PR
flags ^signals\.Event$ (go vet is silent)TESTED · Q7
blocks Step 3 · seed + incidents
obs seed --check
When: every PR, ~1 s
incidents/stripe-outage.yaml: metric checkout.charge not in contract (did you mean checkout.charges?)DECIDED · A193 tab 2
blocks Step 4 · queries work
obs query --static
When: every PR, ~2 s
dashboards/overview.json panel 3: checkout_charges (counter) is stored as checkout_charges_totalDECIDED · A193 tab 2
blocks Step 4 · queries work
obs query --static
When: every PR, ~2 s
alerts/charge-errors/critical.yaml: sum by (user_id) — user_id is a banned metric labelDECIDED · A193 tab 2
blocks Step 4 · queries work
obs query --live
When: observability/** changed
panel 5 "Timeouts": 0 rows; filter error_type="timeout", seed only has [http_502, card_declined]DECIDED · A193 tab 2
blocks Step 4 · queries work
obs query --live
When: observability/** changed
link trace→logs: 0 log lines for trace 4bf92f35… (datasource derived field uses "traceID", logs carry "trace_id")DECIDED · A193 tab 5
blocks Step 5 · alerts fire
obs alerts
When: observability/** changed, ~15 s
checkout-slow/warning fired 12:30 in baseline: false alarmDECIDED · A193 tab 2
comments only Step 6 · dashboards look good
obs look (AI)
When: dashboards changed, ~90 s
3/5 incident visible: the spike is in panel 6 at the bottom; move "Charge errors by provider" to the top row.DECIDED · A193 tab 2
blocks e2e job (after the 6 steps)
obs assert
When: e2e job, ~10 s
checkout.charges: 212 series > budget 30 (new label: payment.card_bin)DECIDED · A193 tab 2
comments only Daily production audit
obs audit
When: daily, read-only, ~20 s
raichu: event checkout.charge.succeeded not seen in 7 days (dead code, or the log level filters it?)DECIDED · A193 tab 2
comments only AI review
obs review
When: e2e + weekly vs production
charge-errors/critical uses >5/s; raichu p50 is 310/s, so it fires on normal traffic. Use a ratio.DECIDED · A193 tab 2
blocks Step 4 · queries work
obs query --live (only if the seed has the real label)
When: observability/** changed
the static check passes it; the panel returns 0 rows on the seedDECIDED · A193 tab 2 PROPOSED · this site
The steps at a glance
The live checks start the small local stack inside the pull request's CI job, only when observability/ changes. DECIDED · A193 tab 2
| Step | Command | When | Time |
|---|---|---|---|
| 1 contract → code | obs gen --check, obs config --check | commit + every PR | ~1 s |
| 2 code uses ports | lint rule per language | commit + every PR | inside lint |
| 3 seed + incidents | obs seed --check | every PR | ~1 s |
| 4 queries work | obs query --static, then --live | static: every PR; live: observability/ | ~2 s; live ~46 s + ~30 s |
| 5 alerts fire | obs alerts | observability/ | ~15 s on the same stack |
| 6 dashboards look good | obs look (comments only) | dashboards change | ~90 s + 1 AI call each |
All rows: DECIDED · A193 tab 2. Stack start time: TESTED · Q15.
Blocks, or only comments
- Blocks the merge: steps 1–5. DECIDED · A193 tab 2
- Only comments:
obs look(step 6) andobs review(AI). DECIDED · A193 tab 1 #11 - Runs outside the pull request:
obs assertin the e2e job,obs auditdaily against production,obs reviewweekly. DECIDED · A193 tab 2 - Example comment:
3/DECIDED · A193 tab 25 incident visible: the spike is in panel 6 at the bottom; move "Charge errors by provider" to the top row.
Extra checks around the 6 steps
| Check | What it proves | When | Time |
|---|---|---|---|
obs assert --run <id> | the real app sent every name, linked | e2e job | ~10 s |
obs scrape | an upstream chart really exposes its metrics | Primordial e2e | ~5 s |
obs audit | production still matches the contract | daily, read-only | ~20 s |
obs review | steps 1 and 3 make sense (AI) | e2e + weekly | 1–3 AI calls |
obs sandbox up | see it yourself: seeds and opens Grafana | by hand | ~46 s |
All rows: DECIDED · A193 tab 2.
What the cheap query check proves (no containers)
- Every name in a query is in the contract or the
auto:list. DECIDED · A193 tab 2 - The stored spelling is right:
_total, unit suffix,durian_cloud_event. DECIDED · A193 tab 2 - No banned label in
by (…)or in filters. Example:user_id. DECIDED · A193 tab 2 - Alerts never select a landscape; every file has at least one query. DECIDED · A193 tab 2
- It cannot check label values. A filtered value must appear in the seed or an incident; that is checked live. DECIDED · A193 tab 2
Alerts: how step 5 works
- Each alert is a small typed file with a JSON schema, so your editor autocompletes it. Example:
datasource: "metric"is flagged: not one of[metrics, logs]. DECIDED · A193 tab 1 #14 - One query source, or an optional second. Example: metrics
$A > 1and logs$B > 10. DECIDED · A193 tab 1 #18 - Recommended (pick 2): obs sends the rendered rule to real Grafana's
POST /at each minute of the seed window, and applies theapi/ v1/ eval for:timer itself. OPEN · A193 pick 2 - The spike proved the eval API runs VictoriaMetrics + Loki + math + threshold at past times. It has no
for:. TESTED · Q16 - The spike ran one query over a past window, not "every minute for 4 hours". TESTED · Q16, backends/q4-grafana-eval NOT TESTED · full loop
- Traces: alert on span metrics, not raw TraceQL (experimental for alerts). DECIDED · A193 tab 2
- In production, the Primordial chart turns each alert file into one Grafana alert; obs uses the same renderer. DECIDED · A193 tab 2
Open pick 2 lives here: Grafana's eval API, our own evaluator, or real-time waits. Options and trade-offs. OPEN · A193 pick 2
Example alert file
# observability/alerts/charge-errors/critical.yaml
title: Charge errors high
queries:
A: { datasource: metrics, expr: 'sum(rate(checkout_charges_total{outcome="error"}[5m]))' }
B: { datasource: logs, expr: 'sum(count_over_time({service_name="checkout"} | durian_cloud_event="checkout.charge.failed" [5m]))' }
condition: $A > 1 && $B > 10
for: 10m
summary: Errors at {{ $values.A.Value }}/s
DECIDED · A193 tab 2
Example seed and incident
# observability/seed.yaml (shortened)
length: 4h
random_seed: 42 # same file → same data
traffic: { requests: 3/s, shape: daily_wave }
metrics:
checkout.charges:
- { where: { payment.provider: stripe, outcome: success }, share: 0.90 }
- { where: { payment.provider: stripe, outcome: declined }, share: 0.07 }
incidents: [stripe-outage, checkout-slow]
# observability/incidents/stripe-outage.yaml (shortened)
story: Stripe returns 502 for 40% of charges for 30 minutes.
window: { start: 90m, duration: 30m }
change:
- { metric: checkout.charges, where: { payment.provider: stripe, outcome: error }, rate: 1.2/s }
expect: { fire: [charge-errors/critical], quiet: [checkout-slow/warning], fire_within: 15m }
- Example values are hand-written, never copied from queries, so the live check proves something. DECIDED · A193 tab 2
Services with no generator (Primordial charts)
- Charts like CloudNativePG get a contract with
generator: none: upstream names, units and budgets only. DECIDED · A193 tab 2 - Step 2 is skipped.
obs scrapein a local Kubernetes test proves the metrics exist instead. DECIDED · A193 tab 2 - Example failure:
cnpg_pg_replication_lag not exposed (renamed to cnpg_pg_replication_lag_seconds?). DECIDED · A193 tab 2