Zum Inhalt springen
Farbschema wählenSprache wählen

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.

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.

  1. (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.

  2. Richte Prometheus auf den Server. Der Server lauscht auf SERVER_PORT (Standard 8080). Füge eine scrape_config hinzu — nimm das Bearer-Token nur auf, wenn du eines gesetzt hast:

    scrape_configs:
    - job_name: supacloud
    metrics_path: /metrics
    scheme: http # https, wenn du TLS vor dem Server terminierst
    # Lass diesen Block ganz weg, wenn SUPACLOUD_METRICS_TOKEN nicht gesetzt ist:
    authorization:
    type: Bearer
    credentials: <derselbe Wert wie SUPACLOUD_METRICS_TOKEN>
    static_configs:
    - targets: ["supacloud.example.com:8080"]
  3. Lade Prometheus neu und bestätige auf der Targets-Seite, dass das supacloud-Target UP ist, prüfe dann, dass eine Serie rendert, z. B. supacloud_http_requests_total.

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.

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.

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.

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.

  1. Importiere das Dashboard. In Grafana, Dashboards → New → Import, und lade observability/grafana/supacloud-s4-dashboard.json hoch. Wähle bei der Abfrage deine Prometheus-Datenquelle (das Dashboard stellt eine datasource-Template-Variable bereit). Es liefert fünf Panels: HTTP Route p95, HTTP Route Throughput, HTTP Route 5xx, DB Pool Saturation und Dispatch SLA + Queue.

  2. Lade die Alert-Regeln. observability/prometheus/supacloud-s4-rules.yml ist eine Prometheus-Regelgruppe. Referenziere sie aus deiner prometheus.yml (rule_files:) oder lade sie in das Grafana-managed Alerting. Sie definiert vier Regeln:

    Alert Feuert, wenn Severity
    SupaCloudHttpRouteP95High das p95 einer Route > 2 s für 15 m warning
    SupaCloudHttpRouteErrors anhaltende 5xx-Rate > 0,05/s für 10 m warning
    SupaCloudDbPoolSaturation supacloud_db_pool_saturation > 0,85 für 10 m warning
    SupaCloudDispatchTickSla supacloud_dispatch_alert{kind="tick_sla"} == 1 für 5 m critical
  3. 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.

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.shk6/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.shk6/s4-ws-streaming.ts) authentifiziert sich am echten Server, empfängt Live- Task-Events über k6/websockets und erfasst Sequenzlücken, Server-Resync-Frames sowie die Recovery über den exakten after_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).

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 pending ist, gilt die bestehende geprüfte Single-Bootstrap-Datei. Sobald es active ist, 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_PCT bleibt als explizite Workflow-Deklaration bestehen, aber jeder Wert außer 20 scheitert fail-closed.
  • Jedes k6-Szenario trägt zusätzlich eine absolute In-Script-p95-Decke (z. B. tasks_board bei 1200 ms, runs_board bei 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.

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.

  1. Lege eine Grafana-PostgreSQL-Datenquelle für dieselbe CI-Trend-Datenbank an. Gib ihrem Datenbankkonto nur Leserechte auf ci_playwright_trend.

  2. Importiere das Flake-Dashboard manuell. Lade unter Dashboards → New → Import observability/grafana/flake-trend-panel.json hoch 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.

  3. Halte Quarantäne außergewöhnlich. web/tests/e2e/quarantine.json ist im Normalzustand leer. Ein temporärer Eintrag muss zu einem expliziten @quarantine-Tag im Testtitel passen und owner, reason sowie ein ablaufendes UTC-review_by tragen. 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.

  4. Lies den Changed-Spec-Burn-in separat. Geänderte Mock- und Real-Specs laufen nächtlich zehnmal auf chromium-desktop, ohne Retries. Der letzte ci-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.

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