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
- Construire des pipelines CAPTCHA côté client
- Mettre en place une automatisation responsable
- Superviser CaptchaAI avec les métriques et alertes Datadog
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 :