DevOps y Escalado

Análisis de logs de resolución de CAPTCHA con ELK Stack

A escala, resolver CAPTCHA se reduce a tres preguntas que debes responder en segundos: ¿cuál es mi tasa de éxito ahora mismo?, ¿qué error se dispara? y ¿qué tan lenta va la cola? Con unos pocos workers, un grep sobre el log alcanza; con miles de resoluciones al día, no. ELK Stack (Elasticsearch, Logstash y Kibana) recoge esos logs, los indexa y te deja buscarlos, agregarlos y graficarlos para aislar una incidencia antes de que un cliente la reporte.

Qué te muestran tus logs de CAPTCHA

  • Piensa en una agencia en Madrid o Ciudad de México que resuelve CAPTCHA para QA de sus flujos de checkout y para monitoreo de precios en marketplaces regionales: el volumen es irregular y el coste mensual en USD es predecible, pero la tasa de resolución no lo es.
  • Cuando esa tasa cae, el equipo necesita saber en minutos si el culpable es un tipo de CAPTCHA concreto, una URL objetivo o toda la cola.
  • Un panel de ELK responde eso sin abrir un servidor por SSH, siempre que hayas registrado los datos correctos desde el primer día.

Qué campos registrar en cada resolución

  • captcha_id — para rastrear un caso puntual de principio a fin.
  • captcha_type — para segmentar reCAPTCHA v2 frente a Turnstile.
  • solve_time — la latencia real medida en tu lado.
  • error_code — el motivo exacto del fallo.
  • target_url — para detectar los sitios que fallan más de la cuenta.
  • poll_count — cuántas veces sondeaste el resultado antes de resolver.
  • Nunca guardes el token de la solución: no aporta valor diagnóstico y sí añade riesgo.

Arquitectura del pipeline de logs

El recorrido es lineal: los workers escriben JSON, Filebeat lo recoge, Logstash lo enriquece y Elasticsearch lo indexa; Kibana consulta encima.

[CAPTCHA Workers] → JSON logs → [Filebeat] → [Logstash] → [Elasticsearch]
                                                                ↓
                                                           [Kibana]

Logging estructurado en JSON

El primer eslabón es el worker: en vez de texto libre, emite cada evento como una línea JSON para que Filebeat y Logstash lo parseen sin expresiones regulares frágiles.

Python: emitir cada evento como 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 (Node.js): el mismo patrón

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

Del log al índice: el pipeline de ingesta

Con los eventos ya en JSON, tres piezas los llevan hasta Elasticsearch. Filebeat vigila el directorio del worker y reenvía cada línea; Logstash parsea el JSON, clasifica la latencia en tramos (fast, medium, slow) y normaliza la marca de tiempo; y la plantilla de índice declara como keyword los campos por los que filtras (tipo, código de error, URL), sin lo cual las consultas se arrastran.

Filebeat: recoger y enviar los logs

# 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"]

Logstash: parsear y clasificar

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

Elasticsearch: plantilla de í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" }
      }
    }
  }
}

Problemas frecuentes y su solución

Problema Causa Solución
Los logs no aparecen en Kibana Filebeat no está enviando los logs Revisa los logs de Filebeat; verifica que el patrón de ruta coincida
Errores al parsear JSON Líneas que no son JSON en el archivo de log Añade json.keys_under_root en Filebeat; corrige la salida del logger
Demasiados índices Índice diario sin ILM Configura Index Lifecycle Management con retención de 30 días
Consultas lentas Falta el mapeo keyword Usa el tipo keyword para los campos filtrables, no text

Paneles de Kibana que valen la pena

  • El panel de tasa de éxito es el primero que abres cuando algo se siente raro.
  • El desglose de errores y la latencia en el tiempo te dicen por qué.
  • Los seis paneles de la tabla siguiente se arman con los campos que ya registras.
Panel Visualización Consulta
Tasa de éxito de resolución Métrica level:info AND message:"Solve success" / total
Desglose de errores Gráfico circular level:error agrupado por error_code
Latencia en el tiempo Gráfico de líneas Promedio de solve_time a lo largo del tiempo
Errores en el tiempo Gráfico de barras Conteo de level:error por intervalos de 5 minutos
Resoluciones más lentas Tabla Top 10 por solve_time descendente
Actividad de la cola Gráfico de área Conteo por message ("Task submitted" frente a "Solve success")

Consultas que resuelven incidencias reales

Estas búsquedas en Kibana Query Language son las que usas cuando cae la tasa de resolución: aíslan por tiempo, tipo, latencia o caso concreto.

# 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"

Preguntas frecuentes

¿Cómo detecto en Kibana que la tasa de resolución empezó a caer?

Compara el panel de tasa de éxito en una ventana móvil (la última hora frente a las 24 previas) y cruza el desglose de errores. Si un error_code sube justo cuando baja la tasa, ahí empiezas; si suben todos a la vez, mira primero la latencia y la cola.

¿Vale la pena montar ELK si ya tengo el panel de CaptchaAI?

Depende del volumen. El panel de CaptchaAI muestra el estado de tu cuenta; ELK correlaciona tus resoluciones con tu infraestructura (latencia, URLs, reintentos) y guarda el histórico. A partir de unos miles de resoluciones diarias, esa correlación propia compensa el montaje.

¿Puedo usar OpenSearch en lugar de Elasticsearch?

Sí. OpenSearch es compatible a nivel de API, así que la salida de Logstash, Filebeat y OpenSearch Dashboards funcionan igual. Los ejemplos se aplican sin cambios.

¿Cuánto tiempo conviene conservar los logs y cómo automatizarlo?

30 días para operación diaria y hasta 90 para análisis de tendencias. No borres a mano: configura Index Lifecycle Management (ILM) para eliminar los índices antiguos y controlar el coste de almacenamiento.

Siguiente paso

Centraliza y analiza tus logs de resolución de CAPTCHA de punta a punta: obtén tu clave API de CaptchaAI y levanta tu stack ELK.

Guías relacionadas:

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