Um pico de erros no CAPTCHA aparece de madrugada — foi timeout do provedor, uma chave sem saldo ou instabilidade de rede? Com grep espalhado em arquivos de log por worker, essa resposta leva minutos, um arquivo de cada vez, e ainda depende de lembrar em qual servidor o worker estava rodando naquela hora.
No ELK Stack (Elasticsearch, Logstash, Kibana), a mesma investigação vira uma consulta de poucos segundos: filtre por error_code, agrupe por captcha_type e leia a tendência de latência direto no painel, sem entrar em nenhum servidor manualmente. Este guia monta o pipeline de ponta a ponta:
- Logs em JSON estruturado nos workers (Python e Node.js).
- Coleta com o Filebeat e enriquecimento no Logstash.
- Indexação no Elasticsearch com mapeamento correto desde o início.
- Painéis e consultas prontas no Kibana para investigar incidentes.
Arquitetura do pipeline de logs de CAPTCHA
[CAPTCHA Workers] → JSON logs → [Filebeat] → [Logstash] → [Elasticsearch]
↓
[Kibana]
O worker grava logs em JSON; o Filebeat encaminha, o Logstash processa e enriquece, e o Elasticsearch indexa para consulta no Kibana. Cada etapa faz um trabalho só — isso facilita achar onde um problema começou quando um worker novo entra na frota e os logs dele não aparecem como esperado no painel.
Estruture os logs de CAPTCHA em JSON antes de tudo
Sem formato estruturado, campos como error_code, solve_time e captcha_type simplesmente não existem para o Elasticsearch indexar depois. Os exemplos abaixo emitem exatamente os mesmos campos em Python e em JavaScript — mantendo os nomes idênticos entre os dois workers, o índice único do Elasticsearch consegue misturar os dois idiomas sem duplicar mapeamento.
Python: log estruturado em JSON
import os
import json
import time
import logging
import sys
import requests
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# Add extra fields
if hasattr(record, "captcha_id"):
log_entry["captcha_id"] = record.captcha_id
if hasattr(record, "captcha_type"):
log_entry["captcha_type"] = record.captcha_type
if hasattr(record, "solve_time"):
log_entry["solve_time"] = record.solve_time
if hasattr(record, "error_code"):
log_entry["error_code"] = record.error_code
if hasattr(record, "target_url"):
log_entry["target_url"] = record.target_url
if hasattr(record, "poll_count"):
log_entry["poll_count"] = record.poll_count
return json.dumps(log_entry)
# Configure logger
logger = logging.getLogger("captchaai")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
session = requests.Session()
def solve_captcha(sitekey, pageurl, captcha_type="recaptcha_v2"):
extra = {"captcha_type": captcha_type, "target_url": pageurl}
# Submit
resp = session.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
logger.error("Submit failed", extra={
**extra, "error_code": data.get("request")
})
return {"error": data.get("request")}
captcha_id = data["request"]
extra["captcha_id"] = captcha_id
logger.info("Task submitted", extra=extra)
# Poll
start = time.time()
poll_count = 0
for _ in range(60):
time.sleep(5)
poll_count += 1
result = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
elapsed = round(time.time() - start, 2)
logger.info("Solve success", extra={
**extra,
"solve_time": elapsed,
"poll_count": poll_count
})
return {"solution": result["request"]}
if result.get("request") != "CAPCHA_NOT_READY":
logger.error("Solve failed", extra={
**extra,
"error_code": result.get("request"),
"poll_count": poll_count
})
return {"error": result.get("request")}
logger.error("Solve timeout", extra={
**extra,
"error_code": "TIMEOUT",
"poll_count": poll_count
})
return {"error": "TIMEOUT"}
JavaScript: o mesmo padrão em Node.js
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
function log(level, message, fields = {}) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
service: "captcha-worker",
...fields,
};
console.log(JSON.stringify(entry));
}
async function solveCaptcha(sitekey, pageurl, captchaType = "recaptcha_v2") {
const fields = { captchaType, targetUrl: pageurl };
const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY, method: "userrecaptcha",
googlekey: sitekey, pageurl, json: 1,
},
});
if (submitResp.data.status !== 1) {
log("error", "Submit failed", { ...fields, errorCode: submitResp.data.request });
return { error: submitResp.data.request };
}
const captchaId = submitResp.data.request;
fields.captchaId = captchaId;
log("info", "Task submitted", fields);
const startTime = Date.now();
let pollCount = 0;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
pollCount++;
const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (pollResp.data.status === 1) {
const solveTime = ((Date.now() - startTime) / 1000).toFixed(2);
log("info", "Solve success", { ...fields, solveTime: parseFloat(solveTime), pollCount });
return { solution: pollResp.data.request };
}
if (pollResp.data.request !== "CAPCHA_NOT_READY") {
log("error", "Solve failed", { ...fields, errorCode: pollResp.data.request, pollCount });
return { error: pollResp.data.request };
}
}
log("error", "Solve timeout", { ...fields, errorCode: "TIMEOUT", pollCount });
return { error: "TIMEOUT" };
}
module.exports = { solveCaptcha };
Envie os logs com o Filebeat
O Filebeat só lê os arquivos *.log do worker e encaminha cada linha JSON ao Logstash — ele não interpreta nem transforma o conteúdo, então qualquer erro de parsing aparece na etapa seguinte, não aqui. Se o worker roda em vários containers ou instâncias, o mesmo filebeat.yml funciona em todos; basta configurar um processor de metadados de host para diferenciar a origem de cada linha no índice.
# filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/log/captcha-worker/*.log
json:
keys_under_root: true
add_error_key: true
message_key: message
output.logstash:
hosts: ["logstash:5044"]
Processe os logs no pipeline do Logstash
O Logstash recebe o payload do Filebeat, faz o parse do JSON e calcula um campo derivado (solve_time_bucket) para facilitar filtros por faixa de latência antes de gravar no Elasticsearch. É também o lugar certo para adicionar novos campos calculados — por exemplo, marcar is_beta_type: true quando captcha_type for CaptchaFox (beta), Friendly Captcha (beta) ou Lemin (beta), para separar esses volumes nos painéis sem mexer no código do worker.
# logstash-captcha.conf
input {
beats {
port => 5044
}
}
filter {
# Parse JSON logs
json {
source => "message"
target => "captcha"
}
# Add computed fields
if [captcha][solve_time] {
mutate {
add_field => {
"solve_time_bucket" => "fast"
}
}
if [captcha][solve_time] > 30 {
mutate { update => { "solve_time_bucket" => "medium" } }
}
if [captcha][solve_time] > 90 {
mutate { update => { "solve_time_bucket" => "slow" } }
}
}
# Extract date
date {
match => ["[captcha][timestamp]", "ISO8601"]
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "captcha-logs-%{+YYYY.MM.dd}"
}
}
Defina o índice no Elasticsearch
O template abaixo fixa o tipo de cada campo do índice captcha-logs-* — sem isso, o Elasticsearch tenta adivinhar o tipo sozinho e error_code/captcha_type acabam mapeados como texto livre, o que quebra os filtros exatos usados nos painéis a seguir. Se a maior parte do seu tráfego é do Brasil, hospedar o cluster numa região como sa-east-1 da AWS reduz a latência de escrita; para volumes menores, number_of_shards: 1 (já usado abaixo) evita overhead desnecessário — só aumente ao passar de alguns GB por dia no índice.
{
"index_patterns": ["captcha-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"captcha_type": { "type": "keyword" },
"captcha_id": { "type": "keyword" },
"error_code": { "type": "keyword" },
"solve_time": { "type": "float" },
"poll_count": { "type": "integer" },
"target_url": { "type": "keyword" },
"level": { "type": "keyword" },
"message": { "type": "text" }
}
}
}
}
Monte os painéis de logs de CAPTCHA no Kibana
Com o índice mapeado, estes seis painéis cobrem o que costuma importar no dia a dia: taxa de sucesso, tipo de erro predominante e latência. Comece só com eles — dá para adicionar painéis por captcha_type ou por região do worker depois, conforme a operação crescer.
| Painel | Visualização | Consulta |
|---|---|---|
| Taxa de sucesso | Métrica | level:info AND message:"Solve success" / total |
| Distribuição de erros | Pizza | level:error por error_code |
| Latência no tempo | Linhas | Média de solve_time |
| Erros no tempo | Barras | level:error a cada 5 min |
| Resoluções mais lentas | Tabela | Top 10 por solve_time |
| Atividade da fila | Área | "Task submitted" vs "Solve success" |
Consultas prontas para investigar logs de CAPTCHA
As consultas abaixo usam os mesmos campos do índice captcha-logs-* — copie e ajuste o intervalo de tempo:
# All errors in the last hour
level:error AND @timestamp:[now-1h TO now]
# Timeout errors for reCAPTCHA
error_code:TIMEOUT AND captcha_type:recaptcha_v2
# Slow solves (> 60 seconds)
solve_time:>60
# Errors for a specific target URL
level:error AND target_url:"example.com"
# Specific CAPTCHA ID investigation
captcha_id:"73519847"
Solução de problemas
| Problema | Causa provável | Como corrigir |
|---|---|---|
| Logs não aparecem no Kibana | Filebeat não está enviando os logs | Verifique o log do Filebeat e o padrão de caminho |
| Erros de parsing de JSON | Linha fora do padrão JSON | Ative json.keys_under_root; corrija a saída do logger |
| Excesso de índices | Índice diário sem política de ILM | Configure ILM com retenção de 30 dias |
| Consultas lentas | Falta mapeamento keyword no campo |
Use keyword, não text, em campos de filtro |
Dica: se nada disso resolver, confirme primeiro que o próprio worker está gravando em
/var/log/captcha-worker/*.log— boa parte dos "problemas no Kibana" começa numa etapa anterior do pipeline.
Antes de ativar em produção
Confira estes pontos antes de apontar o pipeline inteiro para produção:
- Índice
captcha-logs-*criado com o template de mapeamento aplicado. - ILM configurado com a retenção definida (30 ou 90 dias).
- Os seis painéis do Kibana funcionando com dados reais, não só com os exemplos deste guia.
- Nenhum campo sensível (texto da solução, dados pessoais) chegando ao índice.
Perguntas frequentes
Vale a pena montar um ELK Stack só para logs de CAPTCHA, ou o Datadog já resolve?
Depende do volume. Se já paga por SaaS de observabilidade, agregar os logs lá custa menos esforço. Acima de milhões de eventos por mês, um Elasticsearch próprio sai mais barato.
A partir de qual volume de tarefas compensa migrar do grep para o ELK?
Com dezenas de tarefas por dia, grep ainda resolve bem. A partir de alguns milhares por dia, centralizar no Elasticsearch já compensa.
Por quanto tempo devo guardar os logs de CAPTCHA, considerando a LGPD?
30 dias bastam para casos operacionais; 90 dias se precisar de análise de tendência. Configure o ILM do Elasticsearch para excluir índices antigos. Como os logs não devem conter dados pessoais nem o texto da solução (próxima pergunta), o cuidado de conformidade é limitar o acesso ao índice.
Preciso registrar o texto da solução do CAPTCHA nos logs?
Não. É um token de uso único sem valor de diagnóstico — registrá-lo só aumenta custo e risco. Grave apenas metadados: ID, tipo, latência e status.
Coloque o ELK para rodar
Consulte seus logs de CAPTCHA em segundos — obtenha sua chave de API e configure o pipeline ELK.
Guias relacionados:
- Registro estruturado
- Monitoramento com Datadog
- Rastreamento com OpenTelemetry