DevOps & Scaling

Création d'alertes CaptchaAI personnalisées avec PagerDuty

Relier votre pipeline CaptchaAI à PagerDuty tient en trois briques : une règle de gravité par symptôme, un appel à l'API Events v2 pour ouvrir l'incident, et une clé de déduplication pour ne pas noyer l'astreinte. Bien réglé, l'ensemble réveille la bonne personne, avec assez de contexte pour diagnostiquer sans ouvrir les logs.

Quelles alertes déclencher, et à quelle gravité

Tout se joue dans le mapping symptôme → gravité : trop bavarde, l'alerte finit ignorée ; muette, elle laisse passer la panne.

Gravité État Action PagerDuty
Critique Solde < 2 $ Page l'ingénieur d'astreinte
Critique Tous les workers à l'arrêt Page l'ingénieur d'astreinte
Élevé Taux d'erreur > 20 % pendant 5 min Créer un incident urgent
Avertissement Solde < 10 $ Créer un incident basse urgence
Avertissement File d'attente > 100 pendant 10 min Créer un incident basse urgence
Info Latence de résolution p95 > 120 s Ajouter à un incident existant ou journaliser

Le solde surveillé est celui de votre compte CaptchaAI. Sur un forfait d'entrée comme BASIC ($15/mois, 5 threads), un seuil critique à 2 $ laisse le temps de recharger avant l'arrêt. Ajustez les deux seuils à votre débit : une équipe qui consomme vite doit remonter l'avertissement au-dessus de 10 $.

Préparer le service PagerDuty

Avant d'écrire une ligne de code, créez le service et l'intégration qui recevront vos événements : sans clé de routage valide, aucun appel ne remontera.

Étape Action
1 Créer un service PagerDuty nommé « CaptchaAI Pipeline »
2 Ajouter l'intégration Events API v2 à ce service
3 Copier la clé de routage dans la variable d'environnement PAGERDUTY_ROUTING_KEY
4 Définir une politique d'escalade (astreinte → chef d'équipe → manager)
5 Configurer les canaux de notification (push, SMS, appel)
6 Prévoir des fenêtres de maintenance pour les arrêts planifiés

Pour une équipe scraping basée à Lyon dont les workers tournent sur Scaleway ou en région AWS eu-west-3 (Paris), regrouper tout derrière un seul service « CaptchaAI Pipeline » donne à l'astreinte un incident lisible plutôt qu'une avalanche par instance. Pensez RGPD en remplissant custom_details : que des métriques techniques, jamais de données personnelles.

Python : l'API Events v2 de PagerDuty

Une classe ouvre, reconnaît et résout les incidents ; un moniteur calcule le taux d'erreur sur une fenêtre glissante et interroge le solde via res.php. Chaque alerte porte sa propre dedup_key : elle empêche les doublons et permet la résolution automatique au retour à la normale.

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)

La branche else assure la résolution automatique : dès que le solde repasse au-dessus de 10 $, les incidents se referment seuls, sans clôture manuelle.

Node.js : superviser le solde et le taux d'erreur

La même logique se transpose pour les pipelines JavaScript : setInterval remplace la boucle Python, axios porte les requêtes.

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 };

La version Node.js exige plus de 10 mesures avant de déclencher, pour éviter qu'un échec isolé ne fasse sonner l'astreinte ; alignez Python sur la même condition.

Dépannage

Problème Cause Correctif
L'alerte ne se déclenche pas Clé de routage erronée Vérifiez que la clé correspond bien à l'intégration Events API du service
Incidents en double dedup_key absente Définissez toujours une clé de déduplication stable par type d'alerte
Déluge d'alertes Aucun regroupement entre déclenchements La clé de déduplication PagerDuty supprime les doublons : utilisez-la systématiquement
La résolution automatique reste inactive Clés de déduplication différentes Assurez-vous que resolve réutilise la clé exacte du trigger

FAQ

Comment définir les seuils de solde selon mon forfait CaptchaAI ?

Partez de votre consommation quotidienne : seuil critique à une demi-journée de résolution restante, avertissement à un ou deux jours. La facturation étant en dollars US, gardez les seuils en $ pour coller à la valeur renvoyée par getbalance.

Vaut-il mieux passer par PagerDuty directement ou via Datadog/New Relic ?

Les deux marchent. Si vous envoyez déjà vos métriques à Datadog ou New Relic, leurs intégrations natives PagerDuty évitent de dupliquer la logique d'alerte. L'appel direct à l'API Events reste préférable pour un contrôle fin des seuils et du contenu des incidents.

Comment tester l'intégration sans réveiller l'astreinte ?

Déclenchez un incident de gravité info avec une dedup_key de test, puis résolvez-le aussitôt avec la même clé : vous validez la clé de routage et le format du payload sans mobiliser l'escalade. Les fenêtres de maintenance PagerDuty aident aussi pendant les essais.

Que faire si l'appel à l'API Events de PagerDuty échoue ?

Isolez l'appel dans un bloc try et journalisez l'échec plutôt que de laisser l'exception couper la boucle de supervision, comme le fait la version Node.js. Une panne de PagerDuty ne doit jamais arrêter votre pipeline, mais surveillez ces échecs : ils vous privent de visibilité.

Articles connexes

Prochaines étapes

Soyez prévenu à la seconde où votre pipeline CAPTCHA déraille : créez votre clé API CaptchaAI et branchez PagerDuty.

Guides associés :

Les commentaires sont désactivés pour cet article.