Tutorials

Logs de auditoria de CAPTCHA: como rastrear resoluções para conformidade

Quem resolveu qual CAPTCHA, para qual site e a que custo? Quando o time de compliance faz essa pergunta, quem responde é o log de auditoria — não a memória de quem rodou o script. Este guia mostra como registrar cada chamada de resolução feita à CaptchaAI em um formato estruturado, útil tanto para depuração quanto para uma auditoria formal (LGPD incluída, quando há dado pessoal envolvido). Você vai encontrar exemplos completos em Python e Node.js, uma tabela de retenção por volume e um conjunto de FAQs sobre o que registrar — e o que nunca deve entrar no log.

Quais dados entram no log de auditoria de CAPTCHA

Cada resolução deve gerar um registro com estes campos:

Campo Para que serve Exemplo
timestamp Quando a requisição foi feita 2026-04-04T14:30:00Z
request_id Identificador único dessa resolução uuid4()
captcha_type Método CAPTCHA usado userrecaptcha
target_site URL da página onde o CAPTCHA apareceu https://staging.example.com/qa-login
task_id ID da tarefa na CaptchaAI 73829451
status Resultado final solved, failed, timeout
solve_time_ms Tempo entre o envio e o resultado 18432
error_code Código de erro, se houver falha ERROR_CAPTCHA_UNSOLVABLE
initiator Quem ou o que disparou a resolução scraper-job-42
cost Custo estimado da chamada 0.003

Nunca registre:

  • a chave de API;
  • o token do CAPTCHA (expira em minutos e perde valor probatório);
  • dado pessoal identificável vindo do site de destino — se a URL em target_site costuma carregar informação sensível na query string, mascare-a antes de gravar.

LGPD e outras exigências de conformidade

A LGPD entra em cena sempre que o log toca, direta ou indiretamente, em dado pessoal. Como a orientação acima já exclui dado pessoal do site de destino, o log tende a ficar fora do escopo mais sensível da lei — mas confirme com o time jurídico se o volume ou o tipo de operação exige alguma adequação formal. Fora do Brasil, SOC 2, GDPR e HIPAA pedem o mesmo tipo de evidência (quem fez o quê, quando, com que resultado): a mesma implementação atende aos dois cenários, mudando apenas o prazo de retenção e quem tem acesso de leitura ao arquivo.

Como registrar cada resolução em Python

Trate o log de auditoria como um canal separado dos logs de aplicação: um RotatingFileHandler dedicado evita que ruído de debug se misture ao rastro que um auditor vai pedir depois.

# audit_solver.py
import os
import uuid
import time
import json
import logging
from datetime import datetime, timezone
import requests

API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")

# Configure audit logger — separate from application logs
audit_logger = logging.getLogger("captcha_audit")
audit_logger.setLevel(logging.INFO)

# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
    "captcha_audit.jsonl",
    maxBytes=50_000_000,  # 50 MB per file
    backupCount=10,
)
handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(handler)

def log_audit(record):
    """Write a structured audit record."""
    audit_logger.info(json.dumps(record, default=str))

def solve_with_audit(sitekey, pageurl, captcha_type="userrecaptcha",
                      initiator="unknown"):
    """Solve a CAPTCHA with full audit logging."""
    request_id = str(uuid.uuid4())
    start = time.time()

    audit_record = {
        "request_id": request_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "captcha_type": captcha_type,
        "target_site": pageurl,
        "initiator": initiator,
        "status": "submitted",
    }

    session = requests.Session()

    try:
        # Submit
        resp = session.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY,
            "method": captcha_type,
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": "1",
        })
        result = resp.json()

        if result.get("status") != 1:
            audit_record.update({
                "status": "submit_failed",
                "error_code": result.get("request"),
                "solve_time_ms": int((time.time() - start) * 1000),
            })
            log_audit(audit_record)
            return None

        task_id = result["request"]
        audit_record["task_id"] = task_id

        # Poll
        time.sleep(15)
        for _ in range(25):
            poll = session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get",
                "id": task_id, "json": "1",
            })
            poll_result = poll.json()

            if poll_result.get("status") == 1:
                solve_time = int((time.time() - start) * 1000)
                audit_record.update({
                    "status": "solved",
                    "solve_time_ms": solve_time,
                    "cost_estimate": 0.003,  # Adjust per your rate
                })
                log_audit(audit_record)
                return poll_result["request"]

            if poll_result.get("request") != "CAPCHA_NOT_READY":
                audit_record.update({
                    "status": "failed",
                    "error_code": poll_result.get("request"),
                    "solve_time_ms": int((time.time() - start) * 1000),
                })
                log_audit(audit_record)
                return None

            time.sleep(5)

        audit_record.update({
            "status": "timeout",
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        return None

    except Exception as e:
        audit_record.update({
            "status": "error",
            "error_code": str(e)[:200],
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        raise

# Usage
token = solve_with_audit(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://www.google.com/recaptcha/api2/demo",
    initiator="price-scraper-v2",
)

Uma resolução bem-sucedida vira uma linha JSON como esta — fácil de filtrar com grep/jq e de enviar a qualquer agregador sem transformação prévia:

{"request_id":"a1b2c3d4-...","timestamp":"2026-04-04T14:30:00+00:00","captcha_type":"userrecaptcha","target_site":"https://www.google.com/recaptcha/api2/demo","initiator":"price-scraper-v2","status":"solved","task_id":"73829451","solve_time_ms":18432,"cost_estimate":0.003}

Como registrar em Node.js

A mesma lógica em Node.js: o registro só é gravado depois que o resultado (sucesso, falha ou timeout) é conhecido, então o log nunca fica com um estado "pendente" preso no meio do caminho.

// audit_solver.js
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
const AUDIT_FILE = 'captcha_audit.jsonl';

function logAudit(record) {
  fs.appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n');
}

async function solveWithAudit(sitekey, pageurl, initiator = 'unknown') {
  const requestId = uuidv4();
  const start = Date.now();
  const record = {
    request_id: requestId,
    timestamp: new Date().toISOString(),
    captcha_type: 'userrecaptcha',
    target_site: pageurl,
    initiator,
    status: 'submitted',
  };

  try {
    const submit = await axios.get('https://ocr.captchaai.com/in.php', {
      params: {
        key: API_KEY, method: 'userrecaptcha',
        googlekey: sitekey, pageurl, json: '1',
      },
    });

    if (submit.data.status !== 1) {
      record.status = 'submit_failed';
      record.error_code = submit.data.request;
      record.solve_time_ms = Date.now() - start;
      logAudit(record);
      return null;
    }

    record.task_id = submit.data.request;
    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 25; i++) {
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
      });

      if (poll.data.status === 1) {
        record.status = 'solved';
        record.solve_time_ms = Date.now() - start;
        record.cost_estimate = 0.003;
        logAudit(record);
        return poll.data.request;
      }
      if (poll.data.request !== 'CAPCHA_NOT_READY') {
        record.status = 'failed';
        record.error_code = poll.data.request;
        record.solve_time_ms = Date.now() - start;
        logAudit(record);
        return null;
      }
      await new Promise(r => setTimeout(r, 5000));
    }

    record.status = 'timeout';
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    return null;
  } catch (e) {
    record.status = 'error';
    record.error_code = e.message.slice(0, 200);
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    throw e;
  }
}

Como consultar os logs de auditoria

Com o arquivo JSONL em mãos, um resumo diário é só percorrer as linhas e agregar por status. Rode este script como tarefa agendada (cron, GitHub Actions) para ter um panorama sem abrir o arquivo manualmente:

import json
from collections import Counter
from datetime import date

def daily_summary(log_file, target_date=None):
    """Generate a daily summary from audit logs."""
    target = target_date or date.today().isoformat()
    statuses = Counter()
    total_cost = 0
    solve_times = []

    with open(log_file) as f:
        for line in f:
            record = json.loads(line)
            if record["timestamp"].startswith(target):
                statuses[record["status"]] += 1
                total_cost += record.get("cost_estimate", 0)
                if record.get("solve_time_ms"):
                    solve_times.append(record["solve_time_ms"])

    print(f"Date: {target}")
    print(f"Total requests: {sum(statuses.values())}")
    print(f"Statuses: {dict(statuses)}")
    print(f"Estimated cost: ${total_cost:.2f}")
    if solve_times:
        print(f"Median solve time: {sorted(solve_times)[len(solve_times)//2]}ms")

daily_summary("captcha_audit.jsonl")

Quanto tempo guardar e onde armazenar os registros

O volume diário determina se um arquivo local com rotação já basta ou se vale migrar para um agregador dedicado:

Volume Tamanho do registro diário Armazenamento mensal Recomendação
100 resoluções/dia ~30 KB ~1 MB Arquivo local
1.000 resoluções/dia ~300 KB ~10 MB Arquivo local + rotação
10.000 resoluções/dia ~3 MB ~100 MB Enviar para um agregador de log
100.000 resoluções/dia ~30 MB ~1 GB Log centralizado (ELK, Datadog)

Noventa dias costuma bastar para auditoria operacional; para exigência regulatória específica, confirme o prazo antes de configurar o backupCount. Se os workers rodam em nuvem, hospedar o log na mesma região (ex.: sa-east-1, São Paulo) reduz a latência de gravação.

Problemas comuns no log de auditoria

Problema Causa provável Como corrigir
Arquivo de log crescendo demais Rotação não configurada Use RotatingFileHandler ou logrotate
Registros de auditoria faltando Exceção lançada antes do log ser gravado Grave o log dentro de um bloco finally
Gravação lenta em alto volume I/O de arquivo síncrono Use escrita assíncrona ou buffer
Timestamps inconsistentes Desvio do relógio do servidor Sincronize com NTP; grave sempre em UTC

Perguntas frequentes

Log de auditoria substitui o log de aplicação?

Não. O log de aplicação registra o que o sistema fez internamente; o de auditoria registra o que aconteceu em cada resolução — request_id, custo, resultado. São canais com públicos e retenções diferentes.

Por quanto tempo preciso guardar os registros para atender à LGPD ou a outras normas?

Não há um número único: depende da base legal e do setor. Como ponto de partida, 90 dias cobre a maioria dos casos de debug; para exigência regulatória específica, confirme o prazo com o time de compliance.

Como exporto esses logs para uma ferramenta externa como Datadog ou ELK?

O JSONL já é o formato de entrada esperado pela maioria dos agregadores — aponte o agente de coleta (Filebeat, Fluent Bit, o agente do Datadog) para o arquivo captcha_audit.jsonl. Não é preciso reescrever a lógica de log.

O que aparece no log se a resolução falhar no meio do processo?

O status muda para failed ou timeout e o error_code grava o motivo. O solve_time_ms continua sendo calculado até o ponto da falha — mostra se o problema foi rápido (parâmetro errado) ou lento (timeout de polling).

Preciso de um banco de dados dedicado para guardar os logs?

Não, pelo menos não para começar. Um arquivo JSONL com rotação atende volumes de alguns milhares de resoluções por dia. Migre para um banco ou agregador quando o volume justificar a complexidade extra.

Artigos relacionados

Próximas etapas

Adicione rastreabilidade a cada resolução de CAPTCHA — crie sua chave de API da CaptchaAI e comece a gravar o primeiro log ainda hoje.

Guias relacionados:

Os comentários estão desativados para este artigo.