Ninguém quer descobrir às 3h que o pipeline de CAPTCHA está parado há duas horas porque ninguém olhou o dashboard. A resposta curta: conecte a CaptchaAI ao PagerDuty pela Events API v2, defina qual condição vira página e qual vira só um ticket de baixa urgência, e deixe o dedup key do PagerDuty evitar que a mesma falha acorde o time três vezes. Este guia mostra como montar isso, com código pronto em Python e Node.js.
Regra prática: só chama o plantonista para saldo crítico ou workers todos caídos. Tudo o mais vira incidente de baixa urgência ou só um registro — o objetivo é o plantonista confiar no alerta, não silenciar o app de notificação.
O que deve virar página e o que pode esperar
Nem todo problema no pipeline de CAPTCHA merece acordar o engenheiro de plantão. A tabela abaixo é o ponto de partida que já funciona bem em produção — ajuste os limiares conforme o volume da sua operação:
| Gravidade | Condição | Ação no PagerDuty |
|---|---|---|
| Crítico | Saldo < $2 | Chama o engenheiro de plantão |
| Crítico | Todos os workers caídos | Chama o engenheiro de plantão |
| Alto | Taxa de erro > 20% em 5 min | Cria incidente urgente |
| Aviso | Saldo < $10 | Cria incidente de baixa urgência |
| Aviso | Profundidade da fila > 100 por 10 min | Cria incidente de baixa urgência |
| Informativo | Latência de resolução p95 > 120 s | Anexa ao incidente existente ou só registra |
Por que $2 e $10, e não outro valor?
- Abaixo de $10, ainda há margem para agir antes que o crédito acabe de fato.
- Abaixo de $2, a operação para em questão de horas — por isso vira página, não ticket.
- Entre os dois, o saldo pode oscilar sem gerar página a cada variação normal.
Python: enviando eventos para o PagerDuty
A classe abaixo cobre os três eventos que a Events API v2 aceita — trigger, acknowledge e resolve — mais um monitor simples de saldo e taxa de erro que decide quando chamar cada um:
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: a mesma lógica com axios
Mesma ideia, só que com axios e um setInterval em vez de um loop bloqueante — útil se o monitor já roda dentro de um processo Node existente:
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 };
Configurando o serviço no PagerDuty
- Crie um serviço no PagerDuty chamado "CaptchaAI Pipeline".
- Adicione a integração Events API v2 a esse serviço.
- Copie a routing key para a variável de ambiente
PAGERDUTY_ROUTING_KEY. - Configure a política de escalonamento (plantão → líder de equipe → gerente).
- Configure as regras de notificação (push, SMS, telefone).
- Adicione janelas de manutenção para paradas planejadas.
Em produção, o script de monitoramento precisa ficar sempre de pé: um cron a cada minuto, um worker dedicado ou uma função serverless agendada resolvem bem. Se seus workers rodam numa região como sa-east-1 para reduzir latência até o endpoint da CaptchaAI, mantenha o monitor na mesma região — evita confundir RTT com degradação real do solver. E como o custom_details vai para os logs do PagerDuty, coloque só métricas operacionais ali (saldo, taxa de erro, tarefas), nunca dados pessoais — o que também ajuda na conformidade com a LGPD.
Problemas comuns
- Alerta não dispara — causa provável: routing key errada. Confira se a chave corresponde à integração Events API do serviço.
- Incidentes duplicados — causa provável: faltou o
dedup_key. Defina sempre uma chave de deduplicação consistente por tipo de alerta. - Enxurrada de alertas — causa provável: sem cooldown entre disparos. A dedup key do PagerDuty suprime duplicatas — confirme que você está usando.
- Resolução automática não funciona — causa provável: dedup key diferente no resolve. O resolve precisa usar exatamente a mesma chave de deduplicação do trigger.
Perguntas frequentes
Quanto tempo leva para colocar esse alerta no ar?
Menos de uma hora, com conta no PagerDuty e chave de API da CaptchaAI em mãos — a integração básica (saldo + taxa de erro) já sai coberta pelo código deste guia, que trata trigger, acknowledge e resolve.
Como evito que o time fique com fadiga de alerta?
Duas medidas resolvem a maior parte do problema:
- Use
dedup_keypara agrupar falhas repetidas num único incidente, em vez de um alerta por falha. - Deixe avisos de saldo baixo como baixa urgência e reserve a página só para saldo < $2 ou todos os workers parados.
Dá para rodar esse monitor sem manter um servidor ligado o tempo todo?
Sim. Nem run_checks() (Python) nem checkAndAlert() (Node.js) dependem de estado persistente além da janela de erro em memória — funcionam bem como job agendado a cada minuto numa função serverless.
PagerDuty é a única opção, ou dá para alertar direto pelo Datadog ou New Relic?
Não é a única. Datadog e New Relic têm integração nativa com o PagerDuty — se você já envia métricas para uma dessas plataformas, dispare por lá em vez de duplicar a lógica de alerta. A integração direta via API (a deste guia) vale quando você quer controle fino sobre a mensagem e o momento do alerta.
Como testo o alerta sem acordar o time de verdade?
Crie um segundo serviço no PagerDuty só para teste, com sua própria integração Events API v2 e regra de notificação sem push nem SMS, e dispare trigger/acknowledge/resolve manualmente contra essa routing key. Só aponte o monitor para a routing key de produção depois de confirmar que os três eventos chegam certos.
Artigos relacionados
- Como estruturar pipelines de CAPTCHA para clientes com a CaptchaAI
- Automação responsável com a CaptchaAI
- Alertas e métricas da CaptchaAI no Datadog
Próximos passos
Não deixe o pipeline de CAPTCHA falhar em silêncio: comece com uma chave de API da CaptchaAI e conecte o PagerDuty ainda hoje.
Guias relacionados: