Skip to content

feat(monitoring): Prometheus alerts for API saturation (#8840 G) - #8842

Merged
ToddHebebrand merged 3 commits into
mainfrom
feat/8840-saturation-alerts
Oct 16, 2026
Merged

ToddHebebrand merged 3 commits into
mainfrom
feat/8840-saturation-alerts

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

What

Five Prometheus alert rules for API saturation in monitoring/rules/breeze-rules.yml (group breeze-api-alerts), with promtool test rules unit tests in monitoring/rules/tests/breeze-saturation-alerts.test.yml (one fires + one silent case per alert) and the test file added to the perf-rules workflow.

Alert Expression for Severity
ApiDbPoolWaitersHigh breeze_db_pool_admission_waiting > 20 1m warning
ApiDbPoolWaitersHigh breeze_db_pool_admission_waiting > 100 1m critical
ApiDbPoolAcquireTimeouts increase(breeze_db_pool_acquire_timeouts_total[5m]) > 0 0m warning
ApiEventLoopLagHigh breeze_nodejs_eventloop_lag_window_max_seconds > 0.5 3m warning
ApiAdmissionDegraded breeze_admission_level >= 2 2m warning
ApiAdmissionShedding increase(breeze_admission_decisions_total{outcome="shed"}[5m]) > 50 1m warning

Pool and event-loop rules select job=~"breeze-(api|worker).*" and group max/sum by (job, instance), matching DbPoolPermitsHeldByAbandonedTransactions; admission rules are API-only.

Why

On 2026-10-15 a DB-pool stall from one tenant's uploads paged nobody: the waiter gauge climbed, the acquire-timeout counter ticked, sockets closed, and no rule read any of it (#8840 G).

Adjustments against the brief (every metric name verified in apps/api/src)

  • ApiAdmissionDegraded shed counter: the label is outcome with values shed / would_shed (services/admission/admissionMetrics.ts), not decision=~"refuse|shed". Only shed counts; would_shed is observe mode. Split into its own alert, ApiAdmissionShedding, so the level and the volume page separately.
  • ApiEventLoopLagHigh uses for: 3m rather than 2m: _window_max_seconds is a high-water mark over the monitor's 2m retention window (DEFAULT_RETENTION_MS), so one stall holds it up for 2m on its own and for: 2m would page on a single blip.
  • breeze_db_pool_acquire_timeouts_total carries a timer label (on_time/late); summed away in the alert.
  • Dropped AgentWsMassClose: no WS close-by-reason counter exists on main (grep breeze_agent_ws finds only breeze_agent_ws_connections). Left a commented placeholder naming breeze_agent_ws_closed_total{reason} from agentWs: credential lease re-check fails closed on DB timeout and severs healthy sockets fleet-wide (close 4001) #8838.

Test

promtool check rules monitoring/rules/breeze-rules.yml
promtool test rules monitoring/rules/tests/breeze-saturation-alerts.test.yml monitoring/rules/tests/breeze-capacity-kpis.test.yml

Both SUCCESS with local promtool 3.15.0 and with the CI-pinned prom/prometheus:v2.52.0 image. Run against main's rules file, exactly the five fires-cases fail and every silent case passes.

Part of #8840

🤖 Generated with Claude Code

Add five alert rules to the breeze-api-alerts group, each reading a series
the API already publishes, with promtool unit tests (one fires, one silent
per alert) and the test file wired into the perf-rules workflow.

- ApiDbPoolWaitersHigh: breeze_db_pool_admission_waiting > 20 (warning)
  / > 100 (critical) for 1m
- ApiDbPoolAcquireTimeouts: increase(breeze_db_pool_acquire_timeouts_total[5m]) > 0,
  the counter that ticked on 2026-10-15
- ApiEventLoopLagHigh: breeze_nodejs_eventloop_lag_window_max_seconds > 0.5
  for 3m (the gauge is a 2m high-water mark, so 3m outlasts one blip)
- ApiAdmissionDegraded: breeze_admission_level >= 2 for 2m
- ApiAdmissionShedding: increase(breeze_admission_decisions_total{outcome="shed"}[5m]) > 50

AgentWsMassClose is a commented placeholder: no WS close-by-reason counter
exists on main; #8838 plans breeze_agent_ws_closed_total{reason}.

Part of #8840

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 68e46a7
Status: ✅  Deploy successful!
Preview URL: https://e0f97e46.breeze-9te.pages.dev
Branch Preview URL: https://feat-8840-saturation-alerts.breeze-9te.pages.dev

View logs

Todd Hebebrand and others added 2 commits October 15, 2026 15:34
perfWorkflowContract.test.ts asserts the image tag, 'test rules' and the
capacity-kpis test file on one line; the saturation test file continues on
the next line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The level gauge reaches 2 in observe mode (the default) while decide()
admits everything, so "shedding standard traffic" was false there. Say
degraded, and point at ApiAdmissionShedding for actual refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ToddHebebrand
ToddHebebrand added this pull request to the merge queue Oct 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 16, 2026
@ToddHebebrand
ToddHebebrand added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 1d4dce1 Oct 16, 2026
99 of 102 checks passed
@ToddHebebrand
ToddHebebrand deleted the feat/8840-saturation-alerts branch October 16, 2026 19:28
JonathanPitre pushed a commit to JonathanPitre/breeze that referenced this pull request Oct 18, 2026
…#8840) (LanternOps#9212)

## Summary

Implementation plan for the LanternOps#8840 defensive layers. The goal: one
tenant's bad network or workload degrades that tenant, not the shared
API. It is docs only, with no code.

Plan:
`docs/superpowers/plans/platform-ci/2026-10-18-tenant-isolation-bulkheads.md`

Refs LanternOps#8840
Refs LanternOps#8838
Refs LanternOps#8839
Refs LanternOps#8841

## What it plans

| Task / PR | Protection |
|---|---|
| 0 (ops) | Soak, then set `BREEZE_ADMISSION_MODE=enforce` (W08 is
already deployed in observe) |
| A | **Pool lanes + interactive reserve** inside the existing permit
gate (no second pool). Lanes are `interactive`, `agent` and
`background`, set by the authenticating middleware through a leaf
AsyncLocalStorage. The reserve is ~20 % of `DB_POOL_MAX` (35 → 7). A
contract test keeps lane coverage of the auth middlewares exhaustive |
| B | **Agent-lane tenant fair share**: round-robin across orgs, with a
per-org ceiling (default 50 % of shared capacity) that applies only
under contention, so a sole tenant can still use all of it. A waiter
held back only by its own org's ceiling stays out of W08's global
pressure signals and gets `429 TENANT_POOL_SHARE_EXCEEDED`, so one org
can't escalate admission for everyone |
| C, D, E | **Request-long transaction cleanup**, from an audit of all
33 wrapped agent routes. Software ingest stops taking a second permit.
Logs and changes move their Redis quota and decode before the context.
Sessions, peripherals and message-events side effects, and
command-result enqueues, publishes and dispatch, run after commit |
| F | **WS close accounting**: an exhaustive close-reason enum counted
once per socket by a latch, plus the `AgentWsMassClose` alert left as a
TODO in LanternOps#8842 |
| G, H | **Site agent-connectivity signal**: Redis only, from
authenticated signals with a request denominator. Read by a partner
route, an admin route and an AI tool, and shown in an org-record card
and an admin page |
| J | **Per-org agent error report** from `agent_logs` (receipt time,
site-narrowed, cached) |
| gated | **Per-site breaker enforcement** (429), designed but not
scheduled. Go/no-go after ≥ 7 days of observe data |

Each alert ships with the protection it watches, with promtool fire and
silent cases.

## Settled by the advisor quorum

Claude (author) and Codex (`gpt-6.1-sol`, xhigh, read-only).

- **Agreed:**
  - one pool and one gate, with lanes as permit accounting;
  - round-robin plus a ceiling;
  - ALS lanes rather than a lane field on `DbAccessContext`;
  - `503` for global pressure and `429` for tenant throttles;
  - no wire change.
- **Codex amendments adopted:**
  - ceiling waits are kept out of W08's signals;
  - the authenticated tenant is carried independently of RLS scope;
- detached work loses interactive privilege once its request has settled
(a `settled` flag);
  - agent-origin nested acquisitions are fenced out of the reserve.
- **My draft changed to Codex's view:**
- the WS lease TTL **stays 30 s** (a healthy subscriber does not prove a
publish was delivered);
  - closes are counted through a once-per-socket latch;
- breaker **enforcement is gated on evidence**. LanternOps#8850 removed the proven
DB-pin mechanism, and auth failures must never trip a site, because
agent ids are attacker-chosen.

## Invariants called out

- **No new table and no migration.** Breaker and connectivity state live
in Redis.
- **No agent change, so no `MIN_AGENT_VERSION_*`.** Every shipped agent
retries `429`/`503` and honours `Retry-After` inside the call.
- **Three new env vars:** `DB_POOL_INTERACTIVE_RESERVE`,
`DB_POOL_AGENT_TENANT_MAX_SHARE`, `BREEZE_AGENT_CONNECTIVITY_SIGNALS`.
Each has a full inventory row.
- **No BullMQ job.** The worker contract tests do not fire.
`mcp-coverage`, the aiTools contracts and locale parity fire for G, H
and J.

## Decisions for review

- **Reserve default 20 %.** The lab gate in Task A pins it.
- **Tenant ceiling share 0.5.** A starting value, to be tuned from
`breeze_db_pool_agent_tenant_ceiling_waiting`.
- **UI home for the partner signal.** The plan proposes a card on the
organization record page plus a `/admin/agent-connectivity` page.
- **Breaker enforcement go/no-go.** Decided after the Task G soak.

## Testing

Docs only. An Explore pass checked every path, symbol and line ref the
plan cites against `origin/main`: about 110 OK. It found 3 misses, all
fixed:
- the body-rejection metric's reason is `timeout`, not `stalled`;
- the coverage-test regex now catches `authMiddleware` and `helperAuth`;
- an unverified test-count baseline was removed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Todd Hebebrand <todd@lanternops.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant