Tutoriales

Registro estructurado para operaciones CAPTCHA

Un log estructurado convierte cada resolución de CAPTCHA en un evento JSON que puedes filtrar, buscar y usar para disparar alertas por task_id, tipo de CAPTCHA o código de error. Esa es la diferencia entre saber qué falló y quedarte mirando una línea como "Error solving captcha" sin ningún contexto. Cuando un pipeline nocturno empieza a devolver tokens vacíos, un registro estructurado te da el sitio, el intento de sondeo y el tiempo de resolución exactos; el texto plano solo te confirma que algo se rompió.

En esta guía vas a montar, paso a paso:

  • Un logger JSON en Python con structlog y en Node.js con pino.
  • El registro del ciclo de vida completo de una resolución: envío, sondeo y resultado.
  • Filtros con jq y una alerta automática cuando sube la tasa de errores.
  • Qué campos conviene registrar y cuáles no debes tocar nunca.

Qué gana el registro estructurado frente al texto plano

La ventaja no es estética: puedes consultar los eventos como una base de datos, porque cada línea es un objeto con campos nombrados y no texto que parsear con regex frágiles. Cada evento lleva su propio contexto, así que agrupar, contar o alertar deja de depender de scripts de parsing a medida.

Texto sin formato JSON estructurado
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Difícil de analizar Legible por máquina
Búsqueda solo con grep Filtrar por cualquier campo
Sin correlación El task_id enlaza envío → sondeo → inyección

Piensa en una agencia que corre QA de checkout y monitoreo de precios sobre marketplaces regionales (MercadoLibre, Amazon.es) de madrugada. Si la tasa de resolución cae, nadie quiere leer miles de líneas: filtras por event:"captcha_solve_failed" de la última hora y ves al instante qué tipo de CAPTCHA y qué sitio concentran los errores. Con costo mensual predecible en USD y logs que se consultan como datos, esa depuración pasa de horas a segundos.


Qué campos registrar en cada evento

Antes de escribir el logger, decide la forma del evento. Estos son los campos que hacen accionable cada línea: con ellos reconstruyes toda la vida de una resolución sin abrir el código.

Campo Tipo Descripción
event cadena Nombre del evento: captcha_submitted, captcha_solved, etc.
task_id cadena ID de tarea de CaptchaAI para la correlación
captcha_type cadena recaptcha_v2, turnstile, image, etc.
site_url cadena URL de la página de destino
solve_time_ms entero Tiempo total desde el envío hasta la resolución
poll_attempts entero Número de solicitudes de sondeo realizadas
error cadena Código de error de CaptchaAI
token_length entero Longitud del token devuelto

Python: structlog

structlog produce JSON estructurado con muy poca configuración. Añade un timestamp ISO, el nivel de log y el renderizador JSON, y ya tienes un logger listo para producción.

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()

Registrar el ciclo de vida de la resolución

Enlaza el contexto común una sola vez con log.bind() y añade el task_id en cuanto la API te lo devuelve. Así cada evento posterior —envío, sondeo, resultado— arrastra los mismos campos y queda correlacionado sin esfuerzo.

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

Cada llamada deja una traza JSON limpia como esta, lista para indexar en tu stack de logs:

{"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"}

Node.js: pino

En el lado de JavaScript, pino es el logger estándar por su bajo overhead. Con isoTime obtienes marcas de tiempo comparables a las de structlog.

const pino = require('pino');

const log = pino({
  level: 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});

El mismo ciclo de vida en JavaScript

pino usa log.child() en lugar de log.bind(), pero el patrón es idéntico: un logger hijo con el contexto de la tarea, derivado de nuevo cuando llega el task_id.

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;
}

Filtrar y alertar sobre los eventos

Con los eventos ya en JSON, cualquier herramienta que entienda ese formato se convierte en tu tablero de diagnóstico.

Encuentra todos los fallos de la última hora

# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'

Alerta cuando sube la tasa de errores

Calcula la tasa de fallos sobre una ventana móvil y emite un evento de aviso en cuanto supera tu umbral. Ese evento se conecta directo a tu sistema de alertas, sin scripts intermedios.

# 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),
                )

Qué registrar y qué evitar

Un buen log es tan útil por lo que omite como por lo que incluye. Como regla:

  • Registra solo tres eventos de negocio por resolución: captcha_submitted, captcha_solved y captcha_solve_failed.
  • Enlaza el task_id lo antes posible para que toda la traza sea correlacionable.
  • Guarda métricas, no secretos: solve_time_ms, poll_attempts y token_length cuentan la historia sin exponer nada sensible.
  • Nunca registres la API key completa ni el token; trunca los sitekeys como en los ejemplos.
  • No registres cada intento de sondeo: CAPCHA_NOT_READY es un estado esperado y solo genera ruido.

Problemas frecuentes y cómo resolverlos

Problema Causa Solución
Logs demasiado ruidosos Registrar cada intento de sondeo Registra solo los eventos de envío, resolución y fallo
No puedes correlacionar eventos Falta el task_id Enlaza task_id pronto con log.bind() o log.child()
Logs no consultables Formato de texto plano Cambia a JSON con structlog o pino
Datos sensibles en los logs Registrar la API key completa Nunca registres claves API; trunca los sitekeys

Preguntas frecuentes

¿Cómo correlaciono el envío, el sondeo y el resultado de un mismo CAPTCHA?

Vincula el task_id con log.bind() (structlog) o log.child() (pino) en cuanto la API te lo devuelve: todos los eventos posteriores lo arrastran y basta filtrar por ese ID para reconstruir la traza completa.

¿Debo guardar el token del CAPTCHA en el log?

No. Registra solo su longitud con token_length, nunca el token completo: un token puede superar los 500 caracteres, infla el volumen de logs y no aporta nada para depurar.

¿Cómo detecto una caída en la tasa de resolución desde los logs?

Cuenta éxitos y fallos sobre una ventana móvil, como en ErrorRateMonitor, y emite un evento captcha_error_rate_high cuando supera tu umbral; ese evento se conecta directo a tu sistema de alertas.

¿El registro estructurado afecta el rendimiento del worker?

Casi nada si eres selectivo. Loguear tres eventos por resolución y evitar el ruido de CAPCHA_NOT_READY mantiene el volumen bajo; tanto structlog como pino están pensados para escribir JSON con un overhead mínimo.


Crea flujos de resolución de CAPTCHA observables con CaptchaAI

Obtén tu clave API en captchaai.com y registra cada resolución con contexto completo desde la primera petición.


Guías relacionadas

Los comentarios están deshabilitados para este artículo.