DevOps & Skalierung

CaptchaAI-Benachrichtigungen mit PagerDuty

Fällt eine CAPTCHA-Pipeline aus, sollte die zuständige Person es sofort erfahren – nicht erst beim Blick ins Dashboard am nächsten Morgen. Das leistet eine direkte Anbindung an die PagerDuty Events API v2: Ein schlanker Monitor fragt Guthaben und Fehlerquote von CaptchaAI ab und erzeugt bei definierten Schwellenwerten automatisch einen Incident – dedupliziert und selbsttätig wieder aufgelöst, sobald sich die Werte normalisieren. Dieser Leitfaden zeigt die Einrichtung in Python und Node.js.

Welche Ereignisse einen Alert auslösen sollten

Der häufigste Fehler beim Aufsetzen von Alerts ist, alles gleich laut zu machen: Wer nachts wegen einer Warnung geweckt wird, deaktiviert die Benachrichtigungen bald ganz. Ordnen Sie deshalb jeden Zustand vorab einem Schweregrad zu und entscheiden Sie erst dann, ob er jemanden anruft, einen Incident anlegt oder nur protokolliert wird. Die folgende Matrix hat sich als Ausgangspunkt bewährt und lässt sich an Ihren Verbrauch und Ihre Team-Größe anpassen.

Schweregrad Bedingung PagerDuty-Aktion
Kritisch Guthaben < 2 $ On-Call-Engineer per Page alarmieren
Kritisch Alle Worker ausgefallen On-Call-Engineer per Page alarmieren
Hoch Fehlerquote > 20 % über 5 Min. Dringenden Incident erstellen
Warnung Guthaben < 10 $ Incident mit niedriger Dringlichkeit
Warnung Warteschlangentiefe > 100 über 10 Min. Incident mit niedriger Dringlichkeit
Info Lösungslatenz p95 > 120 s An bestehenden Incident anhängen oder loggen

PagerDuty-Dienst und Routing-Key vorbereiten

Der Monitor braucht nur einen einzigen Wert: den Routing-Key. Ihn erhalten Sie, indem Sie in PagerDuty einen Dienst mit Events-API-v2-Integration anlegen. Behandeln Sie ihn wie ein Passwort – legen Sie ihn in einer Umgebungsvariablen ab, niemals fest im Code, und rotieren Sie ihn, wenn er versehentlich in ein Repository gelangt.

Schritt Aktion
1 Legen Sie in PagerDuty einen Dienst „CaptchaAI Pipeline“ an.
2 Fügen Sie dem Dienst die Integration Events API v2 hinzu.
3 Kopieren Sie den Routing-Key in die Umgebungsvariable PAGERDUTY_ROUTING_KEY.
4 Richten Sie eine Eskalationsrichtlinie ein (On-Call → Teamleitung → Management).
5 Konfigurieren Sie die Benachrichtigungsregeln (Push, SMS, Anruf).
6 Hinterlegen Sie Wartungsfenster für geplante Ausfallzeiten.

Testen Sie die Kette einmal aktiv, bevor Sie sich darauf verlassen: Rufen Sie trigger mit einer Test-Zusammenfassung und einem eigenen dedup_key auf, prüfen Sie, ob der Incident samt Benachrichtigung ankommt, und lösen Sie ihn anschließend mit resolve wieder auf. Eine stille Integration, die erst beim echten Ausfall versagt, ist schlimmer als gar keine.

Python: Guthaben und Fehlerquote überwachen

Die Klasse kapselt die drei Aktionen der Events API v2 – trigger, resolve, acknowledge – und kombiniert sie mit einem gleitenden Fenster für die Fehlerquote. Der stabile dedup_key sorgt dafür, dass wiederholte Prüfungen denselben Vorfall aktualisieren statt zu duplizieren. run_checks bündelt beide Prüfungen und wird typischerweise im Minutentakt aufgerufen; record_solve füttert das Fenster nach jeder Lösung mit dem Ergebnis, sodass die Fehlerquote immer die letzten fünf Minuten widerspiegelt.

import os
import time
import hashlib
import requests
from datetime import datetime

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
PAGERDUTY_ROUTING_KEY = os.environ["PAGERDUTY_ROUTING_KEY"]

session = requests.Session()


class CaptchaPagerDuty:
    EVENTS_URL = "https://events.pagerduty.com/v2/enqueue"

    def __init__(self, routing_key):
        self.routing_key = routing_key

    def trigger(self, summary, severity="error", source="captcha-pipeline",
                details=None, dedup_key=None):
        """Trigger a new PagerDuty incident."""
        payload = {
            "routing_key": self.routing_key,
            "event_action": "trigger",
            "payload": {
                "summary": summary,
                "severity": severity,  # critical, error, warning, info
                "source": source,
                "timestamp": datetime.utcnow().isoformat() + "Z",
                "custom_details": details or {}
            }
        }

        if dedup_key:
            payload["dedup_key"] = dedup_key

        resp = requests.post(self.EVENTS_URL, json=payload, timeout=10)
        resp.raise_for_status()
        return resp.json()

    def resolve(self, dedup_key):
        """Resolve an existing incident."""
        payload = {
            "routing_key": self.routing_key,
            "event_action": "resolve",
            "dedup_key": dedup_key
        }
        resp = requests.post(self.EVENTS_URL, json=payload, timeout=10)
        resp.raise_for_status()
        return resp.json()

    def acknowledge(self, dedup_key):
        """Acknowledge an existing incident."""
        payload = {
            "routing_key": self.routing_key,
            "event_action": "acknowledge",
            "dedup_key": dedup_key
        }
        resp = requests.post(self.EVENTS_URL, json=payload, timeout=10)
        resp.raise_for_status()
        return resp.json()


pagerduty = CaptchaPagerDuty(PAGERDUTY_ROUTING_KEY)


class CaptchaMonitor:
    def __init__(self):
        self.error_window = []  # (timestamp, is_error)
        self.window_size = 300  # 5 minutes in seconds

    def record_solve(self, success):
        now = time.time()
        self.error_window.append((now, not success))
        # Prune old entries
        self.error_window = [
            (t, e) for t, e in self.error_window
            if now - t < self.window_size
        ]

    @property
    def error_rate(self):
        if not self.error_window:
            return 0.0
        errors = sum(1 for _, e in self.error_window if e)
        return errors / len(self.error_window)

    def check_balance(self):
        resp = session.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "getbalance", "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return None
        return float(data["request"])

    def run_checks(self):
        """Run all monitoring checks and trigger alerts."""
        # Check balance
        balance = self.check_balance()
        if balance is not None:
            if balance < 2:
                pagerduty.trigger(
                    summary=f"CaptchaAI balance critically low: ${balance:.2f}",
                    severity="critical",
                    dedup_key="captcha-balance-critical",
                    details={"balance": balance, "threshold": 2}
                )
            elif balance < 10:
                pagerduty.trigger(
                    summary=f"CaptchaAI balance low: ${balance:.2f}",
                    severity="warning",
                    dedup_key="captcha-balance-warning",
                    details={"balance": balance, "threshold": 10}
                )
            else:
                # Resolve if balance recovered
                try:
                    pagerduty.resolve("captcha-balance-critical")
                    pagerduty.resolve("captcha-balance-warning")
                except Exception:
                    pass  # No incident to resolve

        # Check error rate
        rate = self.error_rate
        if rate > 0.20:
            total = len(self.error_window)
            errors = sum(1 for _, e in self.error_window if e)
            pagerduty.trigger(
                summary=f"CaptchaAI error rate {rate:.0%} "
                        f"({errors}/{total} in 5 min)",
                severity="error",
                dedup_key="captcha-error-rate-high",
                details={
                    "error_rate": round(rate, 3),
                    "total_tasks": total,
                    "failed_tasks": errors,
                    "window_seconds": self.window_size
                }
            )
        elif rate < 0.05 and len(self.error_window) > 10:
            try:
                pagerduty.resolve("captcha-error-rate-high")
            except Exception:
                pass


monitor = CaptchaMonitor()

# After each solve:
# monitor.record_solve(success=True)

# Run checks every 60 seconds:
# while True:
#     monitor.run_checks()
#     time.sleep(60)

Node.js: Health-Monitor mit der Events API v2

Dieselbe Logik als PagerDutyAlerter plus CaptchaHealthMonitor: setInterval ruft die Prüfung im 60-Sekunden-Takt auf, mit identischen Schwellenwerten wie im Python-Beispiel.

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const PD_ROUTING_KEY = process.env.PAGERDUTY_ROUTING_KEY;
const PD_EVENTS_URL = "https://events.pagerduty.com/v2/enqueue";

class PagerDutyAlerter {
  constructor(routingKey) {
    this.routingKey = routingKey;
  }

  async trigger(summary, severity = "error", details = {}, dedupKey = null) {
    const payload = {
      routing_key: this.routingKey,
      event_action: "trigger",
      payload: {
        summary,
        severity,
        source: "captcha-pipeline",
        timestamp: new Date().toISOString(),
        custom_details: details,
      },
    };
    if (dedupKey) payload.dedup_key = dedupKey;

    const resp = await axios.post(PD_EVENTS_URL, payload, { timeout: 10000 });
    return resp.data;
  }

  async resolve(dedupKey) {
    await axios.post(PD_EVENTS_URL, {
      routing_key: this.routingKey,
      event_action: "resolve",
      dedup_key: dedupKey,
    }, { timeout: 10000 });
  }
}

const alerter = new PagerDutyAlerter(PD_ROUTING_KEY);

class CaptchaHealthMonitor {
  constructor(windowMs = 300000) {
    this.results = [];
    this.windowMs = windowMs;
  }

  record(success) {
    this.results.push({ time: Date.now(), success });
    const cutoff = Date.now() - this.windowMs;
    this.results = this.results.filter((r) => r.time > cutoff);
  }

  get errorRate() {
    if (this.results.length === 0) return 0;
    const errors = this.results.filter((r) => !r.success).length;
    return errors / this.results.length;
  }

  async checkAndAlert() {
    // Balance check
    try {
      const resp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "getbalance", json: 1 },
      });
      if (resp.data.status === 1) {
        const balance = parseFloat(resp.data.request);
        if (balance < 2) {
          await alerter.trigger(
            `CaptchaAI balance critically low: $${balance.toFixed(2)}`,
            "critical",
            { balance },
            "captcha-balance-critical"
          );
        } else if (balance < 10) {
          await alerter.trigger(
            `CaptchaAI balance low: $${balance.toFixed(2)}`,
            "warning",
            { balance },
            "captcha-balance-warning"
          );
        } else {
          await alerter.resolve("captcha-balance-critical").catch(() => {});
          await alerter.resolve("captcha-balance-warning").catch(() => {});
        }
      }
    } catch (err) {
      console.error("Balance check failed:", err.message);
    }

    // Error rate check
    const rate = this.errorRate;
    if (rate > 0.2 && this.results.length > 10) {
      await alerter.trigger(
        `CaptchaAI error rate: ${(rate * 100).toFixed(1)}%`,
        "error",
        { errorRate: rate, totalTasks: this.results.length },
        "captcha-error-rate"
      );
    } else if (rate < 0.05 && this.results.length > 10) {
      await alerter.resolve("captcha-error-rate").catch(() => {});
    }
  }
}

const monitor = new CaptchaHealthMonitor();

// Run checks every 60 seconds
setInterval(() => monitor.checkAndAlert(), 60000);

module.exports = { monitor, alerter };

Den Monitor sinnvoll betreiben

Betreiben Sie den Wächter als eigenen Prozess neben der Scraping-Flotte – etwa auf einem kleinen Hetzner- oder netcup-Server per systemd. Entscheidend ist die Trennung: Fällt die Pipeline aus, muss der Monitor unabhängig weiterlaufen. Da CaptchaAI Thread-basiert abrechnet und ein leeres Guthaben neue Lösungen stoppt, ist die Guthaben-Schwelle der wichtigste Alert. BASIC (15 $/Monat, 5 Threads) genügt für kleine Läufe, größere Flotten fahren mit ADVANCE (90 $/Monat, 50 Threads) oder höher.

Fehlerbehebung

Problem Ursache Lösung
Alert wird nicht ausgelöst Routing-Key falsch oder severity nicht gesetzt PAGERDUTY_ROUTING_KEY prüfen, severity auf "error" setzen
Keine Incidents in PagerDuty Health-Monitor läuft nicht setInterval- bzw. run_checks-Schleife starten, Konsolenausgabe prüfen
Doppelte Incidents dedup_key fehlt oder ändert sich dedup_key stabil und eindeutig halten (z. B. "captcha-error-rate")
Resolve schließt Incident nicht Incident bereits manuell geschlossen PagerDuty-Status prüfen; resolve ist idempotent
Balance-Warnung bleibt aktiv resolve für captcha-balance-warning fehlt Nach Normalisierung des Guthabens explizit auflösen

Häufige Fragen

Wie fragt der Monitor das CaptchaAI-Guthaben ab?

Über den getbalance-Aufruf an https://ocr.captchaai.com/res.php. Bei status: 1 steht der Kontostand im Feld request und wird gegen die Schwellen 2 $ und 10 $ geprüft.

Ab welchem Guthaben sollte ich alarmieren?

Zwei Stufen haben sich bewährt: eine Warnung bei unter 10 $ als Erinnerung zum Aufladen und ein kritischer Incident bei unter 2 $. Passen Sie die Werte an Ihren täglichen Verbrauch an.

Kann ich Alerts nach Schweregrad an verschiedene Teams routen?

Ja. Der severity-Wert (critical, error, warning, info) steuert zusammen mit der Eskalationsrichtlinie, wer benachrichtigt wird – kritische Alerts direkt on-call, Warnungen nur als niedrig-dringlicher Incident.

Lösen sich Incidents automatisch wieder auf?

Ja, sofern Sie denselben dedup_key verwenden. Sinkt die Fehlerquote unter 5 % oder erholt sich das Guthaben, sendet der Monitor ein resolve-Event und schließt den Vorfall.


Verwandte Leitfäden


Kein CAPTCHA-Ausfall soll unbemerkt bleiben: CaptchaAI-API-Schlüssel holen und in wenigen Minuten mit PagerDuty verbinden.

Kommentare sind für diesen Artikel deaktiviert.