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.

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.shk6/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).

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_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.

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