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 nächtlichen E2E- + k6-Lastprüfungen
Abschnitt betitelt „Die nächtlichen E2E- + k6-Lastprüfungen“Ein geplanter Job (.forgejo/workflows/e2e-nightly.yml, Cron 0 6 * * *, auch
auf Abruf über workflow_dispatch ausführbar) bootet einen echten
Backend-Stack — Server, Postgres, Web —, seedet ihn und führt zwei Prüfungen
dagegen aus:
- 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.
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 eine eingecheckte 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. - Ein Szenario, dessen p95 um mehr als
PERF_P95_REGRESSION_PCT(nächtlich:20) Prozent über seiner Baseline regressiert, lässt den Job fehlschlagen. - 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.
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 |
| Die Nacht scheitert an einer fehlenden Baseline | K6_REQUIRE_BASELINE=1 und eine k6/baselines/<scenario>.summary.json fehlt oder hat ein nicht-positives p95 |
Checke eine gültige Baseline-Summary für dieses Szenario ein |