Observability: Prometheus, Grafana und die nächtlichen Lastprüfungen
SupaCloud stellt einen einzelnen Prometheus-/metrics-Endpunkt bereit, liefert
ein fertiges Grafana-Dashboard und eine Alert-Regel-Datei mit und führt nächtlich
eine Real-Backend-Lastprüfung aus, die die p95-Latenz gegen eine eingecheckte
Baseline absichert. Diese Seite verdrahtet alle drei, sodass du Request-
Rate/-Fehler/-Latenz, DB-Pool-Druck und Dispatch-Gesundheit siehst und eine
Performance-Regression erkennst, bevor sie Nutzer erreicht.
/metrics mit Prometheus scrapen
Abschnitt betitelt „/metrics mit Prometheus scrapen“Der Server stellt GET /metrics auf seinem Root-Router bereit (nicht unter
/api), also außerhalb des API-Auth-Stacks und der Governor-Rate-Limit-Schicht.
Er rendert Prometheus-Textexposition (text/plain; version=0.0.4) und macht pro
Anfrage DB- + Docker-Arbeit, scrape ihn also nicht häufiger als nötig.
-
(Empfohlen) Setze ein Metrics-Token, damit der Endpunkt nicht weltweit lesbar ist:
SUPACLOUD_METRICS_TOKEN=<eine lange zufällige Zeichenkette>Siehe die Umgebungsvariablen-Referenz dazu, wo deine Bereitstellung env liest, und den OpenBao-Pfad unter Das OpenBao-Secret-Backend verwenden.
-
Richte Prometheus auf den Server. Der Server lauscht auf
SERVER_PORT(Standard8080). Füge einescrape_confighinzu — nimm das Bearer-Token nur auf, wenn du eines gesetzt hast:scrape_configs:- job_name: supacloudmetrics_path: /metricsscheme: http # https, wenn du TLS vor dem Server terminierst# Lass diesen Block ganz weg, wenn SUPACLOUD_METRICS_TOKEN nicht gesetzt ist:authorization:type: Bearercredentials: <derselbe Wert wie SUPACLOUD_METRICS_TOKEN>static_configs:- targets: ["supacloud.example.com:8080"] -
Lade Prometheus neu und bestätige auf der Targets-Seite, dass das
supacloud-TargetUPist, prüfe dann, dass eine Serie rendert, z. B.supacloud_http_requests_total.
Was der Endpunkt bereitstellt
Abschnitt betitelt „Was der Endpunkt bereitstellt“RED — Rate, Errors, Duration (pro Route)
Abschnitt betitelt „RED — Rate, Errors, Duration (pro Route)“Eine Middleware erfasst jede HTTP-Antwort unter dem gematchten
Axum-Route-Template (z. B. /api/runs/{id}/events), nie dem rohen Pfad — der
Label-Raum ist also durch die Routen-Tabelle plus einen einzelnen
{unmatched}-Fallback begrenzt und wächst nie mit Tenant-IDs oder Query-Strings.
Drei Serien tragen das RED-Signal, gelabelt mit method, route und status:
| Metrik | Typ | Signal |
|---|---|---|
supacloud_http_route_requests_total |
counter | Rate — bediente Anfragen |
supacloud_http_route_errors_total |
counter | Errors — Antworten mit Status ≥ 500 |
supacloud_http_route_duration_seconds |
histogram | Duration — Latenz-Buckets (_bucket/_sum/_count) |
Das Duration-Histogramm nutzt feste le-Buckets von 5 ms bis 60 s, sodass dir
histogram_quantile() ein echtes p95 pro Route gibt. Prozessweite Counter
(supacloud_http_requests_total, supacloud_management_requests_total,
supacloud_webhook_requests_total, supacloud_task_launch_requests_total,
supacloud_scheduler_ticks_total) und Geschäfts-Gauges
(supacloud_tasks_total{status}, supacloud_ai_cost_month_usd,
supacloud_agent_containers_active) runden den Snapshot ab.
DB-Pool-Sättigung
Abschnitt betitelt „DB-Pool-Sättigung“Der PostgreSQL-Connection-Pool wird exportiert, sodass du Pool-Druck siehst, bevor er sich in Request-Latenz verwandelt:
| Metrik | Typ | Signal |
|---|---|---|
supacloud_db_pool_connections{state="active|idle|open"} |
gauge | Verbindungen nach Zustand |
supacloud_db_pool_max_connections |
gauge | Konfiguriertes Pool-Maximum |
supacloud_db_pool_saturation |
gauge | active / max-Verhältnis (0–1) |
Eine supacloud_db_pool_saturation nahe 1.0 bedeutet, dass Anfragen am Pool
anstehen — vergrößere den Pool oder reduziere die Last.
Slow-Query-Signal (Logs, keine Metrik)
Abschnitt betitelt „Slow-Query-Signal (Logs, keine Metrik)“Langsame Statements erscheinen in den Server-Logs, nicht auf /metrics. Der
Pool ist so konfiguriert, dass er jedes Statement, das langsamer als ein Schwellwert
ist, auf WARN-Level über die Slow-Statement-Protokollierung von sqlx loggt. Der
Schwellwert ist SUPACLOUD_SLOW_QUERY_LOG_MS (Standard 250 ms; ein Wert von
null oder ein nicht parsbarer Wert fällt auf 250 zurück). Grep das Server-Log nach
diesen WARN-Zeilen oder schicke die Logs an Loki, um die Queries hinter einem
Route-p95-Spike zu finden.
Dispatch-Operability + Alert-Zustand
Abschnitt betitelt „Dispatch-Operability + Alert-Zustand“Die Gesundheit der Delivery Engine wird exportiert, sodass dasselbe Prometheus auf einen stehengebliebenen Scheduler oder ein wachsendes Backlog alarmieren kann (die menschen-orientierte Ansicht davon lebt in Die Delivery Engine betreiben):
| Metrik | Typ | Signal |
|---|---|---|
supacloud_scheduler_seconds_since_tick |
gauge | Sekunden seit dem letzten Scheduler-Tick (-1 vor dem ersten Tick) |
supacloud_backlog_queued_items |
gauge | Auf Dispatch wartende Backlog-Items |
supacloud_backlog_oldest_queued_age_seconds |
gauge | Alter des ältesten wartenden Items |
supacloud_dispatch_alert{kind="…"} |
gauge | Ausgewerteter Alert-Zustand pro Art (1 = breaching, 0 = ok) |
Die supacloud_dispatch_alert-Arten (tick_sla, queue_age, error_spike,
budget_80, weekly_window_low) werden serverseitig gegen benannte
Konstanten-Schwellwerte ausgewertet, sodass dir der Gauge bereits sagt, ob eine
Regel breached — du musst die Schwellwerte nicht in PromQL nachbilden.
Das Grafana-Dashboard und die Alert-Regeln verdrahten
Abschnitt betitelt „Das Grafana-Dashboard und die Alert-Regeln verdrahten“Beide Artefakte sind im Repo unter observability/ eingecheckt — importiere sie
unverändert.
-
Importiere das Dashboard. In Grafana, Dashboards → New → Import, und lade
observability/grafana/supacloud-s4-dashboard.jsonhoch. Wähle bei der Abfrage deine Prometheus-Datenquelle (das Dashboard stellt einedatasource-Template-Variable bereit). Es liefert fünf Panels: HTTP Route p95, HTTP Route Throughput, HTTP Route 5xx, DB Pool Saturation und Dispatch SLA + Queue. -
Lade die Alert-Regeln.
observability/prometheus/supacloud-s4-rules.ymlist eine Prometheus-Regelgruppe. Referenziere sie aus deinerprometheus.yml(rule_files:) oder lade sie in das Grafana-managed Alerting. Sie definiert vier Regeln:Alert Feuert, wenn Severity SupaCloudHttpRouteP95Highdas p95 einer Route > 2 s für 15 m warning SupaCloudHttpRouteErrorsanhaltende 5xx-Rate > 0,05/s für 10 m warning SupaCloudDbPoolSaturationsupacloud_db_pool_saturation> 0,85 für 10 mwarning SupaCloudDispatchTickSlasupacloud_dispatch_alert{kind="tick_sla"} == 1für 5 mcritical -
Hänge einen Contact-Point an. Route die
severity-/stream: s4-Labels an deinen On-Call-Kanal, sodass der kritische Tick-SLA-Alert pagt und die Warnungen benachrichtigen.
Die E2E- + k6-Lastprüfungen
Abschnitt betitelt „Die E2E- + k6-Lastprüfungen“Der manuelle Qualifikationsworkflow (.forgejo/workflows/e2e-nightly.yml, über
workflow_dispatch) bootet einen echten Backend-Stack — Server, Postgres,
Web —, seedet ihn und führt drei Prüfungen dagegen aus. Der überwiegend rote
Zeitplan bleibt deaktiviert, bis die Lane repariert und ein grüner Lauf geprüft ist:
- Playwright-Real-Backend-E2E (
npm run test:e2e:nightly) treibt die UI gegen den echten Server, nicht gegen Mocks. - k6-Hot-Endpoint-Budgets (
scripts/perf/run-k6-nightly.sh→k6/s4-hot-endpoints.js) belasten die sechs heißesten Lesepfade und sichern ihr p95 ab. - k6-2.0-WebSocket-Streaming (
scripts/perf/run-k6-streaming.sh→k6/s4-ws-streaming.ts) authentifiziert sich am echten Server, empfängt Live- Task-Events überk6/websocketsund erfasst Sequenzlücken, Server-Resync-Frames sowie die Recovery über den exaktenafter_sequence-Cursor als Custom-Metriken.
Die k6-Szenarien decken die Boards und die zwei heißen Nicht-Board-Pfade ab:
tasks_board, projects_board, runs_board, run_events, dispatch_tick
(/api/operator/v1/dispatch/metrics) und mcp_gateway (ein tools/list-Aufruf
an /api/mcp). Jedes läuft mit K6_VUS virtuellen Nutzern für K6_DURATION (die
Nacht nutzt 4 VUs für 2 m).
Der p95-Budget-Gate
Abschnitt betitelt „Der p95-Budget-Gate“Jedes Szenario schreibt eine k6-Summary, die scripts/perf/check-k6-budget.mjs
gegen die eingecheckte, verwaltete Baseline in k6/baselines/ vergleicht:
- Mit
K6_REQUIRE_BASELINE=1(der nächtliche Default) lässt eine fehlende oder nicht-positive Baseline-p95 den Job fehlschlagen — der Gate ist von Tag eins an echt. - Das H4-Manifest wird bei jedem Lauf validiert. Solange es
pendingist, gilt die bestehende geprüfte Single-Bootstrap-Datei. Sobald esactiveist, werden ausschließlich die drei checksum-gepinnten Samples akzeptiert und ihr Median je Metrik verwendet; ein ungültiges Set fällt nie auf die Single-Datei zurück. - Ein Szenario, dessen p95 um mehr als 20 % über der verwalteten Baseline
regressiert, lässt den Job fehlschlagen.
PERF_P95_REGRESSION_PCTbleibt als explizite Workflow-Deklaration bestehen, aber jeder Wert außer20scheitert fail-closed. - Jedes k6-Szenario trägt zusätzlich eine absolute In-Script-p95-Decke (z. B.
tasks_boardbei 1200 ms,runs_boardbei 1500 ms) und einen Fehlerraten-Schwellwert von< 1 %; ein Bruch lässt k6 direkt fehlschlagen.
Wenn pg_stat_statements verfügbar ist, erfasst der Runner zusätzlich das
DB-Call-Delta pro Szenario und schreibt eine db_queries_per_request-Zahl in den
Budget-Report, sodass eine Query-Count-Regression (ein einschleichendes N+1) neben
der Latenz sichtbar ist. Artefakte (k6-Summaries, Server-/Postgres-/Web-Logs, der
Playwright-Report) werden bei jedem Lauf hochgeladen, und ein Fehlschlag postet an
den S4_FAILURE_WEBHOOK, falls konfiguriert.
Zwei unabhängige, manuell gestartete Lanes prüfen langlebigere Fehlerklassen,
ohne Teil des Deploy-Graphen zu werden. soak-weekly.yml läuft 90 Minuten und gatet nach zehn
Minuten Burn-in die lineare Steigung von Server-RSS, offenen File-Deskriptoren,
PostgreSQL-Verbindungen und dem langlebigen Signal aus
scheduler_tick_durations. Ein einzelner Absolutwert gilt ausdrücklich nicht als
Leak-Nachweis. chaos-weekly.yml setzt Toxiproxy vor PostgreSQL- und
WebSocket-Traffic und führt fünf begrenzte Experimente aus: Proxy-Latenz, Pumba
Pause, Pumba Netem, Pumba Kill/Restart und einen PostgreSQL-Restart. Nach jedem
Experiment müssen Health, DB-Query, authentifizierter API-Read und ein echter
WS-Eventstream wiederhergestellt sein.
Diese Lanes brauchen Docker-Socket und Network-Emulation-Rechte des self-hosted
e2e-host-net-Runners. Die Tool-Versionen sind fail-closed (k6 2.0.0,
Toxiproxy 2.12.0, Pumba 1.1.7; der Nettools-Helper ist per Digest gepinnt), und
manuelle main-Läufe verlangen das exakte Server-SHA-Image. Wiederkehrende
Soak-/Chaos-Läufe werden erst nach einem geprüften ersten grünen Lauf aktiviert. Die bestätigte
H4-Governance speichert alle drei erfolgreichen Raw-Sample-Sets mit unveränderbarer
Provenance, Checksums und Review-Metadaten nebeneinander und berechnet den
deterministischen Median je Metrik erst im Gate. Alle Runs müssen unterschiedliche
Identitäten und Inhalte, aber dasselbe exakte SHA und dieselbe deklarierte
Umgebung haben. Nightly erfasst nur Evidenz und kuratiert oder aktualisiert nie
eine eingecheckte Baseline. Solange das H4-Manifest pending ist, bleiben die
bestehende geprüfte Single-Baseline und die unveränderte 20-%-p95-Schwelle
maßgeblich.
Playwright-Flake-Trend und Quarantäne-Governance
Abschnitt betitelt „Playwright-Flake-Trend und Quarantäne-Governance“Der gleiche nächtliche Workflow schreibt für die Real- und Mock-Suite einen
Playwright-JSON-Report. JSON ist hier absichtlich gewählt: Es erhält jeden
results[]-Retry-Versuch, sodass ein fehlgeschlagener erster Versuch mit
anschließendem Pass sichtbar bleibt. JUnit kollabiert diesen Fall zu einem Pass
und bleibt nur für die menschenlesbare Job-Summary erhalten.
Setze das Repository-Secret TREND_DB_DSN auf einen PostgreSQL-DSN für die
dedizierte CI-Trend-Datenbank. Provisioniere sie getrennt von der
SupaCloud-Produktdatenbank und der Forgejo-Datenbank. Der In-Run-Reporter erzeugt und befüllt
ci_playwright_trend idempotent; er schreibt weder eine App-Migration noch in
die SupaCloud-Produktdatenbank. Der DSN ist optional: Fehlt er, bleibt die
Test-Lane grün und meldet einen Skip. Ist er konfiguriert, ist eine kaputte
Verbindung oder ein fehlgeschlagener Insert ein echter Instrumentierungsfehler.
-
Lege eine Grafana-PostgreSQL-Datenquelle für dieselbe CI-Trend-Datenbank an. Gib ihrem Datenbankkonto nur Leserechte auf
ci_playwright_trend. -
Importiere das Flake-Dashboard manuell. Lade unter Dashboards → New → Import
observability/grafana/flake-trend-panel.jsonhoch und wähle die PostgreSQL-Datenquelle. Wähle Repository und Lane explizit. Ein Lauf zählt nur dann als Flake, wenn ein früherer Versuch fehlschlug, in ein Timeout lief oder unterbrochen wurde und der letzte Versuch bestand; ein endgültig roter Lauf bleibt ein Fehler und erhöht nicht die Flake-Rate. Rollfenster und Serien sind nach Repository, Lane und Test-ID getrennt. Die Panels zeigen außerdem den p95-Dauer-Drift und gehashte First-Line-Error-Cluster. Fehlertext wird nicht in der Trend-Tabelle gespeichert. -
Halte Quarantäne außergewöhnlich.
web/tests/e2e/quarantine.jsonist im Normalzustand leer. Ein temporärer Eintrag muss zu einem expliziten@quarantine-Tag im Testtitel passen undowner,reasonsowie ein ablaufendes UTC-review_bytragen. Der Validate-Job lehnt veraltete IDs, abgelaufene Einträge und Tag-/Listen-Drift ab. PR-/Main-Lanes schließen nur dieses explizite Tag aus; die nächtlichen Quarantäne-Lanes führen markierte Mock- und Real-Tests fünfmal ohne Retries aus. -
Lies den Changed-Spec-Burn-in separat. Geänderte Mock- und Real-Specs laufen nächtlich zehnmal auf
chromium-desktop, ohne Retries. Der letzteci-green/web-lint-Marker ist der bevorzugte Diff-Anker, der Commit von vor 24 Stunden der Fallback. Kann der Diff nicht berechnet werden, läuft die vollständige zutreffende Suite, statt still zu skippen.
Troubleshooting
Abschnitt betitelt „Troubleshooting“| Symptom | Wahrscheinliche Ursache | Was prüfen |
|---|---|---|
Prometheus-Target ist DOWN mit 401 |
SUPACLOUD_METRICS_TOKEN gesetzt, aber die Scrape-Konfiguration hat das falsche/kein Bearer-Token |
Die authorization.credentials stimmt exakt mit dem env-Wert überein |
/metrics ist öffentlich lesbar |
Kein Token konfiguriert (per Design offen) | Setze SUPACLOUD_METRICS_TOKEN und spiegle es in der Scrape-Konfiguration |
| Route-p95-Panel ist flach / leer | Noch kein Traffic, oder du hast einen rohen Pfad abgefragt | Serien sind nach dem Route-Template gelabelt; prüfe, dass supacloud_http_route_duration_seconds_bucket existiert |
| Eine Route ist langsam, aber die Ursache ist undurchsichtig | Die langsame Query steht in den Logs, nicht auf /metrics |
Grep das Server-Log nach WARN-Slow-Statement-Zeilen; senke SUPACLOUD_SLOW_QUERY_LOG_MS, um das Netz zu weiten |
SupaCloudDispatchTickSla feuert |
Scheduler-/Dispatch-Tick ist nicht im SLA-Fenster vorangekommen | supacloud_scheduler_seconds_since_tick; siehe Die Delivery Engine betreiben |
| Nightly scheitert an der Baseline-Governance | Während H4 pending ist, fehlt eine k6/baselines/<scenario>.summary.json oder ihr p95 ist nicht positiv; bei aktivem H4 ist Manifest, Provenance, Review, Checksum oder Drei-Sample-Vertrag ungültig |
Pending: stelle die geprüfte Single-Datei wieder her. Aktiv: führe k6-baseline-governance.mjs validate aus und korrigiere/reviewe das Evidenz-Set statt zurückzufallen |
| Das Playwright-Flake-Dashboard ist leer | TREND_DB_DSN fehlt/ist falsch oder Grafana zeigt auf Produkt-/Forgejo-Speicher |
Prüfe den Ingest-Hinweis im Workflow und die Read-only-Datenquelle gegen ci_playwright_trend in der dedizierten CI-Trend-Datenbank |
Validate lehnt quarantine.json ab |
Eintrag abgelaufen, Test-ID verschwunden, Inventory geändert oder Tag/Liste driften | Generiere web/tests/e2e/test-inventory.json neu und entferne die Quarantäne oder aktualisiere den befristeten Owner-Review |