Skip to content

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:

PfadInhalt
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.shsetzt Umgebungsvariablen in die Konfigurationen von Prometheus/Alertmanager ein
ci/monitoring/outage-test.shlokaler 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.

MetrikQuelle
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_upRedis (Consumer der Gruppe, aktiv = Abfrage in den letzten 120 s)
pollyou_rollup_pending_events, pollyou_rollup_oldest_pending_event_age_secondsSnapshot des Schedulers
pollyou_messenger_failed_messagesSnapshot (Failure-Transport)
pollyou_webhook_deliveries{status="pending"}, _overdue, _failed_last_hourSnapshot
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_connectionsSnapshot (pg_stat_activity)
pollyou_ops_snapshot_timestamp_seconds, pollyou_ops_snapshot_present, pollyou_ops_collect_duration_secondsSnapshot: 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üssel

DNS: 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

MonitorPrüfung
APICHECK_API_URL (/health), Inhalt "status":"ok"
DashboardCHECK_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-DirektlinkCHECK_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-kuma

Wiederherstellen: 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 -d
  • APP_NETWORK=captain-overlay-network: das Overlay-Netz von CapRover ist „attachable“, der Agent erreicht die Apps unter srv-captain--….
  • API_DNS_NAME=tasks.srv-captain--pollyou-production-api: Swarm liefert unter tasks.<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/write mit 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 (nach ALERT_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.

AlertStufeBedingung (vereinfacht)
PollYouApiDowncriticalkein API-Container antwortet dem Agenten, 1 min
PollYouApiInstanceDownwarningein API-Container antwortet nicht, 2 min
PollYouEndpointDowncriticalein Kuma-Monitor (außer Mercure) down, 1 min
PollYouMercureDowncriticalKuma-Monitor Mercure down, 1 min
PollYouMonitoringAgentSilentcriticalkeine Daten vom Agenten, 3 min
PollYouHttp5xxRatecritical> 2 % 5xx und > 0,05 5xx/s, 5 min
PollYouCollectLatencyHighwarningp95 collect/public_pages > 500 ms, 10 min
PollYouApiLatencyHighwarningp95 dashboard/public_api/auth > 2 s, 10 min
PollYouPhpWorkersSaturatedwarning≥ 90 % der FrankenPHP-Worker belegt, 10 min
PollYouNoWorkercriticalkein aktiver Messenger-Worker, 3 min
PollYouSchedulerDowncriticalSnapshot älter als 3 min, weitere 2 min
PollYouSchedulerSnapshotMissingcriticalkein Snapshot (über eine Stunde oder nie), 5 min
PollYouMessengerRedisDowncriticalAPI erreicht den Queue-Redis nicht, 1 min
PollYouQueueBacklog / …Criticalwarning / criticalälteste async-Nachricht > 5 / > 15 min
PollYouPriorityQueueDelayedwarningälteste Nachricht in async_priority > 60 s, 5 min
PollYouQueueGrowingwarningQueue > 1000 und wachsend, 15 min
PollYouRollupLagwarningältestes ungezähltes Ereignis > 10 min, 5 min
PollYouFailedMessageswarningneue Nachrichten im Failure-Transport (30 min)
PollYouWebhooksOverdue / PollYouWebhookFailureswarningfällige Zustellungen > 5 min liegen geblieben / > 20 gescheitert pro Stunde
PollYouJobStuck / PollYouJobsQueuedwarningExport/KI-Analyse läuft > 30 min / wartet > 15 min
PollYouNotificationFailureswarning> 5 gescheiterte Berichte/Alert-Mails pro Stunde
PollYouPostgresDown / PollYouRedisDowncriticalExporter erreicht die Datenbank / eine Redis-Instanz nicht
PollYouDbConnectionsHighwarning> 80 % von max_connections, 5 min
PollYouRedisQueueMemoryHighcriticalQueue-Redis > 80 % von maxmemory (noeviction)
PollYouGotenbergDown, PollYouExporterDownwarningZiel antwortet nicht, 5 min
PollYouDiskSpaceLow / …Critical / PollYouDiskFillsWithin24hwarning / critical< 15 % / < 5 % frei / Hochrechnung
PollYouMemoryLowwarning< 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 lassen

Der 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

  1. Kuma die API auf der Statusseite als gestört zeigt (Screenshot, /api/status-page/heartbeat/pollyou),
  2. Prometheus PollYouApiDown (über den Agenten) und PollYouEndpointDown[API] (Kumas Sicht) auslöst,
  3. 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.