Appearance
Monitoring und Statusseite
PollYou überwacht sich mit einem selbst gehosteten Stack, der unabhängig vom Hosting-Anbieter funktioniert:
- Uptime Kuma prüft die öffentlichen URLs von außen und betreibt die öffentliche Statusseite (deutsch).
- Prometheus sammelt die Metriken (Anwendung, Queues, Scheduler, FrankenPHP, PostgreSQL, Redis, Gotenberg, Host) und wertet die Alert-Regeln aus.
- Alertmanager benachrichtigt per E-Mail und Slack-kompatiblem Webhook (Slack, Mattermost, Rocket.Chat, Discord über
/slack), jeweils auch mit der Entwarnung. - Grafana zeigt zwei vorkonfigurierte Dashboards.
Was bei einem Alert zu tun ist, steht im Runbook.
Aufbau
┌──────────────── Monitoring-Server (eigene kleine VM) ─────────────────┐
Besucher ───────▶ │ Caddy (TLS) ─▶ Uptime Kuma: Statusseite status.… │
│ ─▶ Grafana │
│ ─▶ Prometheus ◀── Remote Write (HTTPS, Basic Auth) ──┐ │
│ │ Regeln ─▶ Alertmanager ─▶ E-Mail, Webhook │ │
│ └─ scrapt Kumas monitor_status │ │
│ Uptime Kuma ── HTTP-Checks von außen ──┐ │ │
└────────────────────────────────────────┼────────────────────────────┼────┘
▼ │
┌──────────────── App-Server (CapRover) ─────────────────────────────┼────┐
│ api ×n (:8080 /internal/metrics, :9180 Caddy/FrankenPHP) ◀── Agent│ │
│ worker ×n, scheduler ×1, PostgreSQL, Redis queue/cache, │ │
│ Gotenberg, Mercure Prometheus-Agent + Exporter ────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘Warum ein eigener Server? Fällt der App-Server aus, müssen Statusseite und Alarmierung weiterlaufen. Kuma prüft deshalb von außen, und Prometheus merkt das Schweigen des Agenten (PollYouMonitoringAgentSilent). Ein kleiner Server (1–2 vCPU, 2 GB RAM, 20 GB Platte) genügt, idealerweise bei einem anderen Anbieter oder in einem anderen Rechenzentrum.
Warum ein Agent auf dem App-Server? Die Request-Metriken liegen im APCu jedes API-Containers. Über den Load-Balancer abgefragt, käme jede Abfrage bei einem anderen Container an und die Zähler wären unbrauchbar. Der Agent (Prometheus im Agent-Modus) läuft im Docker-Netz der App, fragt jeden Container einzeln ab (DNS tasks.<service>) und schickt alles per Remote Write an den Monitoring-Server. Exporter für PostgreSQL und Redis brauchen ohnehin Zugang zum internen Netz. Keine Metrik-Schnittstelle wird öffentlich erreichbar gemacht.
Dateien:
| Pfad | Inhalt |
|---|---|
deploy/monitoring/server/ | Compose-Projekt des Monitoring-Servers, Regeln, Alertmanager, Grafana, Caddy |
deploy/monitoring/server/kuma-provision/ | Provisionierung von Uptime Kuma als Code (Monitore, Statusseite, API-Schlüssel) |
deploy/monitoring/agent/ | Compose-Projekt des Agenten auf dem App-Server (Prometheus-Agent + Exporter) |
deploy/monitoring/render-config.sh | setzt Umgebungsvariablen in die Konfigurationen von Prometheus/Alertmanager ein |
ci/monitoring/outage-test.sh | lokaler Abnahmetest: simulierter Ausfall → Statusseite + Alarm → Entwarnung |
Metriken der Anwendung
/internal/metrics (API)
GET /internal/metrics mit Authorization: Bearer <METRICS_TOKEN> liefert das Prometheus-Textformat. Ohne METRICS_TOKEN antwortet der Endpunkt mit 404, mit falschem Token mit 403 (wie der Ask-Endpunkt des TLS-Edge). Auf Kunden-Domains ist er nicht erreichbar, das Dashboard leitet /internal nicht weiter.
| Metrik | Quelle |
|---|---|
pollyou_http_requests_total{route_class, status_class} | dieser Container (APCu) |
pollyou_http_request_duration_seconds{route_class} (Histogramm) | dieser Container (APCu) |
pollyou_messenger_queue_messages, _pending, _delayed, _oldest_message_age_seconds {transport} | Redis-Streams messages/messages_priority, live |
pollyou_messenger_consumers{transport, state}, pollyou_messenger_workers_active, pollyou_messenger_redis_up | Redis (Consumer der Gruppe, aktiv = Abfrage in den letzten 120 s) |
pollyou_rollup_pending_events, pollyou_rollup_oldest_pending_event_age_seconds | Snapshot des Schedulers |
pollyou_messenger_failed_messages | Snapshot (Failure-Transport) |
pollyou_webhook_deliveries{status="pending"}, _overdue, _failed_last_hour | Snapshot |
pollyou_jobs{kind, status}, pollyou_jobs_oldest_age_seconds (Exporte, KI-Analysen) | Snapshot |
pollyou_notification_deliveries_open, _failed_last_hour (Berichte, Alert-Mails) | Snapshot |
pollyou_db_connections{role}, pollyou_db_max_connections | Snapshot (pg_stat_activity) |
pollyou_ops_snapshot_timestamp_seconds, pollyou_ops_snapshot_present, pollyou_ops_collect_duration_seconds | Snapshot: Zeitpunkt = Herzschlag des Schedulers |
Routenklassen: collect (Collector-API der Embeds), public_api (API v1), auth, dashboard (übrige /api), public_pages (/s, /p, /r, /oembed, Logos, Abmeldelinks), health, internal, other.
Entscheidung Row-Level Security: Rollup-Verzug, Webhooks, Jobs und Verbindungen sind Zählungen über alle Organisationen. Die darf nur die Worker-Rolle im System-Scope sehen, und den gibt es in HTTP-Requests grundsätzlich nicht (runAsSystem() wirft dort). Deshalb sammelt der Scheduler diese Werte alle 30 Sekunden (CollectOperationalMetrics, Rolle pollyou_worker, System-Scope) und legt einen Snapshot im Cache-Redis ab; /internal/metrics gibt nur diesen Snapshot aus. Nebeneffekt: Der Zeitstempel des Snapshots ist der Herzschlag des Schedulers (Alert PollYouSchedulerDown). Zum Nachsehen oder bei ausgefallenem Scheduler: bin/console app:monitoring:collect im Worker-Container gibt die Werte aus, --store legt den Snapshot zusätzlich ab. Der Rollup-Verzug der Antworten steckt in der Queue: Rollup-Nachrichten werden erst nach dem Commit ihres Batches bestätigt, also zeigt pollyou_messenger_queue_oldest_message_age_seconds{transport="async"} den Verzug, ohne die partitionierte response-Tabelle abzusuchen.
Worker-Erkennung: Jeder Worker-Container liest die Streams unter seinem eigenen Consumer-Namen. Er fragt async_priority mindestens einmal pro Sekunde ab und erneuert laufende Nachrichten alle 30 s (--keepalive); beides setzt die idle-Zeit des Consumers zurück. pollyou_messenger_workers_active zählt die Consumer mit idle ≤ 120 s. Consumer abgebauter Container bleiben in Redis stehen und zählen nicht mehr.
Caddy und FrankenPHP (Port 9180)
Das Produktions-Image aktiviert die Caddy-Metriken (CADDY_GLOBAL_OPTIONS=metrics) und liefert sie auf dem internen Port 9180 aus (backend/docker/php/Caddyfile), ebenfalls nur mit Authorization: Bearer <METRICS_TOKEN>. Ohne Token setzt der Entrypoint ein zufälliges, der Port bleibt also zu. CapRover leitet nur Port 8080 weiter, 9180 ist nur im Overlay-Netz erreichbar. Wichtige Reihen: frankenphp_busy_workers/frankenphp_total_workers (Auslastung der PHP-Worker), caddy_http_request_duration_seconds.
Einrichtung
1. Monitoring-Server
bash
git clone … pollyou && cd pollyou/deploy/monitoring/server
cp .env.example .env && $EDITOR .env # Domains, Passwörter, SMTP, Webhook-URL
docker run --rm caddy:2.10.0 caddy hash-password --plaintext '<Passwort>' # → BASIC_AUTH_HASH
docker compose up -d # mit COMPOSE_PROFILES=public inkl. Caddy/TLS
docker compose run --rm kuma-provision # Monitore, Statusseite, API-SchlüsselDNS: status.…, grafana.…, prometheus.…, alertmanager.… als A-Record auf den Monitoring-Server. Die Statusseite ist unter https://status.…/ öffentlich (Weiterleitung auf /status/pollyou), Grafana hat einen eigenen Login, Prometheus und Alertmanager sind per Basic Auth geschützt. Die Ports ohne TLS hängen nur an 127.0.0.1 (ssh -L 9090:127.0.0.1:9090 …).
kuma-provision ist idempotent: nach jeder Änderung an URLs oder Intervallen einfach erneut ausführen. Es legt beim ersten Lauf das Admin-Konto an, erstellt oder aktualisiert die Monitore
| Monitor | Prüfung |
|---|---|
API | CHECK_API_URL (/health), Inhalt "status":"ok" |
Dashboard | CHECK_DASHBOARD_URL, Inhalt id="app" |
Einbettungen (CDN) | CHECK_EMBED_URL (/v1/embed.js), HTTP 2xx |
Live-Ergebnisse (Mercure) | CHECK_MERCURE_URL (/healthz), HTTP 2xx |
Umfrage-Direktlink | CHECK_DIRECT_LINK_URL (/s/<publicId>), Inhalt <html – optional |
(Intervall KUMA_INTERVAL, eine Wiederholung vor „down“), speichert die Statusseite STATUS_PAGE_SLUG mit deutschen Texten und den Gruppen „Umfragen und Einbettungen“ / „Dashboard und Live-Ergebnisse“ und schreibt einen API-Schlüssel nach /provision/kuma-api-key, mit dem Prometheus Kumas /metrics abfragt (monitor_status). Die Monitor-Namen verwenden die Alert-Regeln, also nicht umbenennen. Von Hand angelegte Monitore (z. B. TLS-Ablauf, Kunden-Domains, Backup-Heartbeat als Push-Monitor) lässt das Skript in Ruhe.
Für den Direktlink eine Beispielumfrage in einer internen Organisation veröffentlichen (Plan internal: braucht kein Guthaben und läuft unbefristet – auf standard würde die Umfrage nach einem Jahr schließen und der Check fehlschlagen) und deren /s/<publicId> eintragen. Sie prüft Datenbank, Templates und Branding auf dem öffentlichen Pfad, den Teilnehmende sehen.
Warum Uptime Kuma 1.23 und ein Skript? Kuma hat keine offizielle Konfiguration als Code. kuma-provision spricht die Socket.IO-API über uptime-kuma-api an, und diese Bibliothek unterstützt Kuma bis 1.23. Ein Upgrade auf Kuma 2 geht erst, wenn das Skript dafür angepasst ist.
Backup von Kuma: Alles Weitere (Verlauf, Wartungsfenster, von Hand angelegte Monitore, Benutzer) liegt in der SQLite-Datenbank im Volume kuma_data. Sicherung, z. B. nächtlich per Cron:
bash
docker compose stop uptime-kuma
docker run --rm -v pollyou-monitoring_kuma_data:/data -v "$PWD/backups:/backup" alpine \
tar czf "/backup/kuma-$(date +%F).tar.gz" -C /data .
docker compose start uptime-kumaWiederherstellen: Volume leeren, Archiv zurückspielen, starten. Ohne Backup baut kuma-provision Monitore und Statusseite neu auf, nur der Verlauf fehlt dann. Prometheus- und Grafana-Daten müssen nicht gesichert werden: Regeln und Dashboards liegen im Repository, die Zeitreihen sind nach PROMETHEUS_RETENTION ohnehin weg.
2. API: Token setzen
In der API-App METRICS_TOKEN setzen (Secret, nur A-Z a-z 0-9 - _, siehe Deployment). Worker und Scheduler brauchen es nicht.
3. Agent auf dem App-Server
bash
cd pollyou/deploy/monitoring/agent
cp .env.example .env && $EDITOR .env
docker compose up -dAPP_NETWORK=captain-overlay-network: das Overlay-Netz von CapRover ist „attachable“, der Agent erreicht die Apps untersrv-captain--….API_DNS_NAME=tasks.srv-captain--pollyou-production-api: Swarm liefert untertasks.<service>eine Adresse pro Container. So fragt der Agent jede API-Instanz einzeln ab, auch nach dem Skalieren.- PostgreSQL: einen eigenen Login nur mit Leserechten auf die Statistik anlegen, nie den Schema-Eigentümer verwenden:sql
CREATE ROLE pollyou_monitor LOGIN PASSWORD '…' IN ROLE pg_monitor; - Redis: Adressen und Passwörter beider Instanzen (Queue und Cache).
REMOTE_WRITE_URL=https://prometheus.…/api/v1/writemit dem Basic-Auth-Konto des Monitoring-Servers.
Kontrolle: In Prometheus (https://prometheus.…/targets zeigt nur die eigenen Ziele) per Abfrage up{environment="production"}. Alle Jobs (pollyou-agent, pollyou-api, pollyou-caddy, postgres, redis, gotenberg, node) müssen 1 liefern.
4. Alarmierung prüfen
bash
# Testalarm direkt an den Alertmanager (verschwindet nach 5 Minuten)
curl -u pollyou:… -H 'Content-Type: application/json' https://alertmanager.…/api/v2/alerts -d '[{
"labels": {"alertname": "PollYouTestAlert", "severity": "warning", "environment": "production"},
"annotations": {"summary": "Testalarm", "description": "Bitte ignorieren."}}]'E-Mail und Chat-Nachricht müssen ankommen und auf das Runbook verlinken (RUNBOOK_URL#<alertname>).
Alerting
Regeln: deploy/monitoring/server/prometheus/rules/pollyou.yml. Jeder Alert hat ein Label severity:
critical: Nutzer sind jetzt betroffen. Benachrichtigung sofort (nachALERT_GROUP_WAIT), Wiederholung stündlich.warning: eingeschränkt oder kurz vor dem Ausfall, Wiederholung alle 4 Stunden.
Beide Stufen gehen an E-Mail und Webhook, die Entwarnung („BEHOBEN“) ebenfalls. Unterdrückt wird, was sich aus einem anderen Alert erklärt: Ist die API ganz weg (PollYouApiDown), schweigen 5xx-, Latenz-, Scheduler- und Worker-Alerts. Schweigt der Agent, bleiben nur Kumas Außensicht-Alerts aktiv. Zwei Kanäle: Eine E-Mail-Adresse (Verteiler des Bereitschaftsteams) und ein Slack-kompatibler Incoming Webhook (ALERT_SLACK_WEBHOOK_URL; Mattermost/Rocket.Chat verstehen dasselbe Format). Für weitere Ziele (z. B. Pager per SMS) einen Receiver in alertmanager.yml.tmpl ergänzen und per routes an severity="critical" hängen.
| Alert | Stufe | Bedingung (vereinfacht) |
|---|---|---|
PollYouApiDown | critical | kein API-Container antwortet dem Agenten, 1 min |
PollYouApiInstanceDown | warning | ein API-Container antwortet nicht, 2 min |
PollYouEndpointDown | critical | ein Kuma-Monitor (außer Mercure) down, 1 min |
PollYouMercureDown | critical | Kuma-Monitor Mercure down, 1 min |
PollYouMonitoringAgentSilent | critical | keine Daten vom Agenten, 3 min |
PollYouHttp5xxRate | critical | > 2 % 5xx und > 0,05 5xx/s, 5 min |
PollYouCollectLatencyHigh | warning | p95 collect/public_pages > 500 ms, 10 min |
PollYouApiLatencyHigh | warning | p95 dashboard/public_api/auth > 2 s, 10 min |
PollYouPhpWorkersSaturated | warning | ≥ 90 % der FrankenPHP-Worker belegt, 10 min |
PollYouNoWorker | critical | kein aktiver Messenger-Worker, 3 min |
PollYouSchedulerDown | critical | Snapshot älter als 3 min, weitere 2 min |
PollYouSchedulerSnapshotMissing | critical | kein Snapshot (über eine Stunde oder nie), 5 min |
PollYouMessengerRedisDown | critical | API erreicht den Queue-Redis nicht, 1 min |
PollYouQueueBacklog / …Critical | warning / critical | älteste async-Nachricht > 5 / > 15 min |
PollYouPriorityQueueDelayed | warning | älteste Nachricht in async_priority > 60 s, 5 min |
PollYouQueueGrowing | warning | Queue > 1000 und wachsend, 15 min |
PollYouRollupLag | warning | ältestes ungezähltes Ereignis > 10 min, 5 min |
PollYouFailedMessages | warning | neue Nachrichten im Failure-Transport (30 min) |
PollYouWebhooksOverdue / PollYouWebhookFailures | warning | fällige Zustellungen > 5 min liegen geblieben / > 20 gescheitert pro Stunde |
PollYouJobStuck / PollYouJobsQueued | warning | Export/KI-Analyse läuft > 30 min / wartet > 15 min |
PollYouNotificationFailures | warning | > 5 gescheiterte Berichte/Alert-Mails pro Stunde |
PollYouPostgresDown / PollYouRedisDown | critical | Exporter erreicht die Datenbank / eine Redis-Instanz nicht |
PollYouDbConnectionsHigh | warning | > 80 % von max_connections, 5 min |
PollYouRedisQueueMemoryHigh | critical | Queue-Redis > 80 % von maxmemory (noeviction) |
PollYouGotenbergDown, PollYouExporterDown | warning | Ziel antwortet nicht, 5 min |
PollYouDiskSpaceLow / …Critical / PollYouDiskFillsWithin24h | warning / critical | < 15 % / < 5 % frei / Hochrechnung |
PollYouMemoryLow | warning | < 10 % Arbeitsspeicher verfügbar, 10 min |
Regeln prüfen: docker run --rm -v "$PWD/deploy/monitoring/server/prometheus/rules:/rules" --entrypoint promtool prom/prometheus:v3.5.0 check rules /rules/pollyou.yml. Nach Änderungen curl -X POST http://127.0.0.1:9090/-/reload auf dem Monitoring-Server (oder Neustart).
Grafana
Datenquelle und Dashboards werden beim Start provisioniert (Ordner „PollYou“):
- PollYou – Betrieb: API-Container, aktive Worker, Alter des Scheduler-Snapshots, Rollup-Verzug, Failure-Transport, Kuma-Zeitleiste, Requests/s, 5xx-Quote und p95 je Routenklasse, FrankenPHP-Auslastung, Queues, Jobs, Webhooks, DB-Verbindungen; Alerts als Annotationen.
- PollYou – Infrastruktur: CPU, Speicher, Platte, Netz, PostgreSQL (Verbindungen, Transaktionen, Größe, Deadlocks), Redis (Speicher, Befehle, Clients, Streams), Scrape-Ziele.
Änderungen an den Dashboards im Repository vornehmen (deploy/monitoring/server/grafana/dashboards/*.json). In der Oberfläche geänderte Dashboards lassen sich nicht speichern (provisioniert), also als JSON exportieren und einchecken.
Lokaler Abnahmetest
bash
make monitoring-test # = ci/monitoring/outage-test.sh: ca. 10 Minuten inkl. Image-Build, räumt danach auf
KEEP_STACKS=1 ci/monitoring/outage-test.sh # beide Stacks danach laufen lassenDer Test startet die Produktions-Images als Projekt pollyou-monitoring-check (Ports 8610–8614, eigene Image-Tags, kollidiert nicht mit make prod-check oder dem Lasttest) und veröffentlicht per k6 eine Beispielumfrage. Daneben startet er Monitoring-Server und Agent als Projekt pollyou-monitoring-test (Kuma 8620, Prometheus 8621, Alertmanager 8622, Grafana 8623, Mailpit für Alert-Mails 8624, Webhook-Empfänger 8625; Einstellungen in ci/monitoring/monitoring.env) und provisioniert Kuma. Wenn alles grün ist, stoppt er den API-Container und wartet, bis
- Kuma die API auf der Statusseite als gestört zeigt (Screenshot,
/api/status-page/heartbeat/pollyou), - Prometheus
PollYouApiDown(über den Agenten) undPollYouEndpointDown[API](Kumas Sicht) auslöst, - Alertmanager die Mail an Mailpit und die Nachricht an den Webhook schickt.
Danach startet er die API wieder und wartet auf „up“ in Kuma sowie die Entwarnung („[BEHOBEN] PollYouApiDown“) per Mail und Webhook. Zeiten, Screenshots der Statusseite (vorher, Störung, danach), Alerts, Mails und Logs landen in .monitoring-results/<Zeit>/ (OUT_DIR). Mit den lokalen Intervallen (Kuma 20 s, Scrape 10 s, for: 1m, group_wait 10 s) zeigt die Statusseite den Ausfall nach etwa 20–40 s, die Mail kommt nach etwa 1,5–2 Minuten. Produktiv (Kuma 30 s, Scrape 15 s, group_wait 30 s) dauert es ein bis zwei Minuten länger.