Um log estruturado transforma cada resolução de CAPTCHA em um registro JSON com task_id, tipo do desafio, tempo de resolução e código de erro — e é isso que permite responder "por que falhou?" com uma consulta, em segundos. Se sua aplicação hoje escreve "Error solving captcha" em texto plano, você não tem dados: tem uma frase.
Abaixo, o ciclo de envio e consulta da API da CaptchaAI instrumentado em Python (structlog) e em Node.js (pino), com filtros jq e um alerta por taxa de erro. O padrão serve a qualquer tipo suportado — reCAPTCHA v2/v3, Turnstile, GeeTest v3 e imagem/OCR.
Por que texto plano não resolve
A diferença não é estética. Texto plano só se lê com grep; JSON se consulta, agrega e correlaciona em qualquer ferramenta de observabilidade.
| Texto plano | JSON estruturado |
|---|---|
Captcha solved in 12.3s |
{"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300} |
| Difícil de analisar | Legível por máquina |
| Busca só com grep | Filtro por qualquer campo |
| Sem correlação | O task_id liga envio → consulta → injeção do token |
O ganho decisivo é a última linha. Uma resolução não é um evento único: é um envio para in.php, consultas a res.php e a injeção do token. Com o task_id vinculado desde o começo, os três momentos viram uma linha do tempo consultável.
Passo 1: configure o logger em Python com structlog
Faça o logger emitir JSON. Com structlog, basta encadear processadores: carimbo de tempo ISO 8601, nível do evento e um renderizador JSON no fim.
import structlog
import time
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.PrintLoggerFactory(),
)
log = structlog.get_logger()
O TimeStamper em ISO evita o problema de fuso quando os workers rodam em sa-east-1 (São Paulo) e o painel está em UTC. Converta só na visualização.
Passo 2: instrumente o ciclo de vida da resolução
A ideia central é o bind: anexe o contexto uma vez e todos os eventos seguintes já saem com esses campos. A sitekey é truncada e a chave de API nunca entra no log.
import requests
API_KEY = "YOUR_API_KEY"
def solve_captcha(captcha_type, sitekey, page_url, proxy=None):
solve_log = log.bind(
captcha_type=captcha_type,
site_url=page_url,
sitekey=sitekey[:12] + "...",
)
# Submit
start = time.time()
solve_log.info("captcha_submit_start")
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}).json()
if resp["status"] != 1:
solve_log.error("captcha_submit_failed", error=resp["request"])
return None
task_id = resp["request"]
submit_ms = int((time.time() - start) * 1000)
solve_log = solve_log.bind(task_id=task_id)
solve_log.info("captcha_submitted", submit_ms=submit_ms)
# Poll
for attempt in range(24):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}).json()
if result["status"] == 1:
solve_ms = int((time.time() - start) * 1000)
solve_log.info(
"captcha_solved",
solve_time_ms=solve_ms,
poll_attempts=attempt + 1,
token_length=len(result["request"]),
)
return result["request"]
if result["request"] != "CAPCHA_NOT_READY":
solve_log.error(
"captcha_solve_failed",
error=result["request"],
poll_attempts=attempt + 1,
)
return None
solve_log.warning("captcha_solve_timeout", poll_attempts=24)
return None
Saída:
{"event":"captcha_submit_start","captcha_type":"recaptcha_v2","site_url":"https://example.com","sitekey":"6Le-wvkSAAAA...","timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_submitted","task_id":"71845302","submit_ms":245,"timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_solved","task_id":"71845302","solve_time_ms":18230,"poll_attempts":4,"token_length":580,"timestamp":"2025-07-15T10:30:18Z","level":"info"}
Três eventos, um task_id, a linha do tempo completa. As consultas intermediárias não geram registro — é o equilíbrio entre ter dados e afogar o disco.
Passo 3: o mesmo padrão em Node.js com pino
Em Node.js o equivalente é o pino: emite JSON por padrão e é rápido o bastante para o caminho crítico.
const pino = require('pino');
const log = pino({
level: 'info',
timestamp: pino.stdTimeFunctions.isoTime,
});
O log.child() cumpre o papel do bind do structlog: cria um logger derivado que carrega o contexto da tarefa.
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function solveCaptcha(captchaType, sitekey, pageUrl) {
const taskLog = log.child({
captchaType,
siteUrl: pageUrl,
sitekey: sitekey.substring(0, 12) + '...',
});
const start = Date.now();
taskLog.info('captcha_submit_start');
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: {
key: API_KEY, method: 'userrecaptcha',
googlekey: sitekey, pageurl: pageUrl, json: 1,
},
});
if (submit.data.status !== 1) {
taskLog.error({ error: submit.data.request }, 'captcha_submit_failed');
return null;
}
const taskId = submit.data.request;
const boundLog = taskLog.child({ taskId });
boundLog.info({ submitMs: Date.now() - start }, 'captcha_submitted');
for (let attempt = 1; attempt <= 24; attempt++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
});
if (poll.data.status === 1) {
boundLog.info({
solveTimeMs: Date.now() - start,
pollAttempts: attempt,
tokenLength: poll.data.request.length,
}, 'captcha_solved');
return poll.data.request;
}
if (poll.data.request !== 'CAPCHA_NOT_READY') {
boundLog.error({ error: poll.data.request, pollAttempts: attempt }, 'captcha_solve_failed');
return null;
}
}
boundLog.warn({ pollAttempts: 24 }, 'captcha_solve_timeout');
return null;
}
Se a stack mistura as duas linguagens, mantenha os mesmos nomes de evento e de campo nos dois lados: captcha_solved precisa significar a mesma coisa em ambos, senão o painel agregado mente.
Referência de campos de log
Padronize este conjunto mínimo antes de instrumentar serviço por serviço — é o que mantém o log consultável meses depois.
| Campo | Tipo | Descrição |
|---|---|---|
event |
string | Nome do evento: captcha_submitted, captcha_solved, etc. |
task_id |
string | ID da tarefa na CaptchaAI, para correlação |
captcha_type |
string | recaptcha_v2, turnstile, image, etc. |
site_url |
string | URL da página do desafio |
solve_time_ms |
integer | Tempo entre envio e resolução |
poll_attempts |
integer | Consultas de resultado feitas |
error |
string | Código de erro retornado pela CaptchaAI |
token_length |
integer | Comprimento do token retornado |
Com captcha_type e solve_time_ms gravados, você compara o comportamento por tipo — reCAPTCHA v2 e Turnstile têm perfis distintos — e decide com dados se o timeout está curto.
Passo 4: filtre os eventos e alerte sobre falhas
Com JSON no disco, o jq já resolve a investigação do dia a dia. Este filtro isola as falhas de resolução:
# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'
Trocar .event por captcha_solve_timeout ou agrupar por .captcha_type são variações de uma linha. Já o alerta deve olhar a taxa de erro em janela móvel, não a falha isolada:
# Count errors vs successes in a rolling window
from collections import deque
class ErrorRateMonitor:
def __init__(self, window_size=100, threshold=0.2):
self.results = deque(maxlen=window_size)
self.threshold = threshold
def record(self, success):
self.results.append(success)
if len(self.results) >= 50:
error_rate = 1 - sum(self.results) / len(self.results)
if error_rate > self.threshold:
log.warning(
"captcha_error_rate_high",
error_rate=round(error_rate, 3),
window=len(self.results),
)
Um limite de 20% em janela de 100 tarefas é um ponto de partida razoável; calibre pelo seu histórico. Ligue o captcha_error_rate_high ao canal de plantão e pare de descobrir o problema pelo ticket do cliente.
Cenário prático: QA autorizado antes do deploy
Um time de e-commerce em São Paulo roda testes de integração em staging.example.com a cada deploy; o formulário de cadastro tem reCAPTCHA v2 e a suíte usa a CaptchaAI nesse ambiente próprio. Quando a suíte fica vermelha, o time abre o solve_time_ms do dia e responde em um minuto se a lentidão veio da resolução ou do banco da aplicação.
Um lembrete de conformidade: site_url e cabeçalhos de requisição podem carregar dados pessoais. Considere as obrigações da LGPD (RGPD, em Portugal) ao definir a retenção — chave de API, token completo e dados de usuário final não pertencem ao log.
Solução de problemas
| Problema | Causa | Correção |
|---|---|---|
| Logs verbosos demais | Registro de toda consulta | Registre só envio, resolução e falha |
| Eventos não se correlacionam | task_id ausente |
Vincule o task_id com log.bind() ou log.child() |
| Logs não pesquisáveis | Formato de texto plano | Migre para JSON com structlog ou pino |
| Dado sensível no log | Chave de API completa | Nunca registre a chave; trunque a sitekey |
| Tempo de resolução sempre alto | Timeout de consulta mal calibrado | Compare solve_time_ms por captcha_type |
Se o log mostra o token chegando e a automação ainda falha, o problema não é mais da resolução: investigue a injeção na página.
Perguntas frequentes
Devo registrar o token do CAPTCHA?
Registre o comprimento, não o conteúdo. Tokens passam de 500 caracteres e não têm valor de diagnóstico depois de expirados — token_length já indica se veio algo plausível.
Por quanto tempo devo guardar esses logs?
O suficiente para investigar uma regressão: 14 a 30 dias de detalhe, com as métricas agregadas guardadas por mais tempo. Defina isso com a política de dados da empresa.
Isso funciona para Cloudflare Turnstile e outros tipos?
Sim. Os campos são agnósticos ao tipo; muda apenas o valor de captcha_type e o método enviado à API. Vale para reCAPTCHA v2/v3, Turnstile, GeeTest v3 e imagem/OCR. CaptchaFox (beta), Friendly Captcha (beta) e Lemin (beta) seguem o mesmo padrão.
Preciso de um plano maior para instrumentar tudo?
Não. O log fica do lado do cliente e não consome créditos. O que define o plano é a concorrência: a CaptchaAI cobra por thread, com resoluções ilimitadas por thread — o BASIC (US$ 15/mês, 5 threads) atende suítes de QA; o ADVANCE (US$ 90/mês, 50 threads) cobre pipelines paralelos.
Como testo a instrumentação sem depender de um site em produção?
Use um formulário próprio em staging, com dados fictícios. Assim você exercita envio, consulta e injeção do token sem tocar em site de terceiros.
Comece a medir suas resoluções de CAPTCHA
Pegue sua chave de API em captchaai.com e coloque o primeiro evento captcha_solved no seu log ainda hoje.