DevOps y Escalado

Plantillas de panel de Grafana para métricas CaptchaAI

¿Tu pipeline de resolución de CAPTCHA está sano o solo lo supones? Sin un panel, estás adivinando. Estas plantillas de Grafana te dan de un vistazo los cuatro números que importan: la tasa de resolución, los percentiles de latencia, el saldo y el desglose de errores. Todas usan Prometheus como fuente de datos y están listas para importar.

Piensa en una agencia en Ciudad de México o Buenos Aires que corre scraping para varios clientes y factura en USD: necesita ver al instante si la tasa de resolución baja o si el saldo se acerca al umbral antes de que una campaña nocturna se frene.

Cómo se estructura el panel: cuatro filas

┌───────────────────────────────────────────────┐
│ Row 1: Overview                               │
│ [Solve Rate %] [Balance $] [Queue Depth] [TPM]│
├───────────────────────────────────────────────┤
│ Row 2: Performance                            │
│ [Latency P50/P95/P99]  [Solve Rate Over Time] │
├───────────────────────────────────────────────┤
│ Row 3: Errors                                 │
│ [Error Rate %]  [Error Breakdown by Type]      │
├───────────────────────────────────────────────┤
│ Row 4: Workers                                │
│ [Active Workers]  [Tasks Per Worker]           │
└───────────────────────────────────────────────┘

Exponer las métricas a Prometheus

Antes de dibujar nada en Grafana, tu solver debe publicar métricas que Prometheus pueda leer. El cliente de ejemplo instrumenta la función que resuelve el CAPTCHA y expone cinco métricas:

  • captcha_solves_total: contador de intentos, etiquetado por tipo y resultado (éxito, error o timeout).
  • captcha_solve_duration_seconds: histograma con la latencia de cada resolución.
  • captcha_balance_dollars: gauge con el saldo de tu cuenta.
  • captcha_queue_depth: tareas pendientes en la cola.
  • captcha_workers_active: número de workers en marcha.

Con esas cinco tienes todo lo que las consultas PromQL de más abajo necesitan.

Python: instrumentar el solver con prometheus-client

import os
import time
import requests
from prometheus_client import (
    Counter, Histogram, Gauge, start_http_server
)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Define metrics
captcha_solves = Counter(
    "captcha_solves_total",
    "Total CAPTCHA solve attempts",
    ["captcha_type", "status"]
)
captcha_latency = Histogram(
    "captcha_solve_duration_seconds",
    "CAPTCHA solve latency",
    ["captcha_type"],
    buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300]
)
captcha_balance = Gauge(
    "captcha_balance_dollars",
    "CaptchaAI account balance"
)
captcha_queue_depth = Gauge(
    "captcha_queue_depth",
    "Pending tasks in queue"
)
captcha_workers_active = Gauge(
    "captcha_workers_active",
    "Number of active workers"
)

session = requests.Session()


def solve_with_metrics(sitekey, pageurl, captcha_type="recaptcha_v2"):
    start = time.time()

    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:
        captcha_solves.labels(captcha_type, "error").inc()
        return {"error": data.get("request")}

    captcha_id = data["request"]
    for _ in range(60):
        time.sleep(5)
        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 = time.time() - start
            captcha_solves.labels(captcha_type, "success").inc()
            captcha_latency.labels(captcha_type).observe(elapsed)
            return {"solution": result["request"]}

        if result.get("request") != "CAPCHA_NOT_READY":
            captcha_solves.labels(captcha_type, "error").inc()
            return {"error": result.get("request")}

    captcha_solves.labels(captcha_type, "timeout").inc()
    return {"error": "TIMEOUT"}


def update_balance():
    resp = session.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance", "json": 1
    })
    if resp.json().get("status") == 1:
        captcha_balance.set(float(resp.json()["request"]))


# Start metrics server on port 9090
start_http_server(9090)

JavaScript: la misma instrumentación con prom-client

const promClient = require("prom-client");
const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const register = new promClient.Registry();

const solvesTotal = new promClient.Counter({
  name: "captcha_solves_total",
  help: "Total CAPTCHA solve attempts",
  labelNames: ["captcha_type", "status"],
  registers: [register],
});

const solveLatency = new promClient.Histogram({
  name: "captcha_solve_duration_seconds",
  help: "CAPTCHA solve latency",
  labelNames: ["captcha_type"],
  buckets: [5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300],
  registers: [register],
});

const balance = new promClient.Gauge({
  name: "captcha_balance_dollars",
  help: "CaptchaAI account balance",
  registers: [register],
});

const queueDepth = new promClient.Gauge({
  name: "captcha_queue_depth",
  help: "Pending tasks in queue",
  registers: [register],
});

async function solveWithMetrics(sitekey, pageurl, captchaType = "recaptcha_v2") {
  const end = solveLatency.startTimer({ captcha_type: captchaType });

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

    if (resp.data.status !== 1) {
      solvesTotal.inc({ captcha_type: captchaType, status: "error" });
      return { error: resp.data.request };
    }

    const captchaId = resp.data.request;
    for (let i = 0; i < 60; i++) {
      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: captchaId, json: 1 },
      });
      if (poll.data.status === 1) {
        end();
        solvesTotal.inc({ captcha_type: captchaType, status: "success" });
        return { solution: poll.data.request };
      }
      if (poll.data.request !== "CAPCHA_NOT_READY") {
        solvesTotal.inc({ captcha_type: captchaType, status: "error" });
        return { error: poll.data.request };
      }
    }
    solvesTotal.inc({ captcha_type: captchaType, status: "timeout" });
    return { error: "TIMEOUT" };
  } catch (err) {
    solvesTotal.inc({ captcha_type: captchaType, status: "error" });
    throw err;
  }
}

// Expose metrics endpoint
const express = require("express");
const app = express();
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});
app.listen(9090);

Consultas PromQL para cada panel

Estas son las consultas de cada panel (el tipo recomendado va entre paréntesis).

Fila 1: vista general

Tasa de resolución (panel Stat)

sum(rate(captcha_solves_total{status="success"}[5m]))
/
sum(rate(captcha_solves_total[5m]))

* 100

Saldo (panel Gauge)

captcha_balance_dollars

Profundidad de la cola (panel Stat)

captcha_queue_depth

Tareas por minuto (panel Stat)

sum(rate(captcha_solves_total[5m])) * 60

Fila 2: rendimiento

Percentiles de latencia (serie temporal)

# p50
histogram_quantile(0.50, rate(captcha_solve_duration_seconds_bucket[5m]))

# p95
histogram_quantile(0.95, rate(captcha_solve_duration_seconds_bucket[5m]))

# p99
histogram_quantile(0.99, rate(captcha_solve_duration_seconds_bucket[5m]))

Tasa de resolución a lo largo del tiempo (serie temporal)

sum(rate(captcha_solves_total{status="success"}[5m])) by (captcha_type) * 60

Fila 3: errores

Tasa de error (serie temporal)

sum(rate(captcha_solves_total{status!="success"}[5m]))
/
sum(rate(captcha_solves_total[5m]))

* 100

Desglose de errores (gráfico circular)

sum by (status) (increase(captcha_solves_total{status!="success"}[1h]))

Fila 4: workers

Workers activos (serie temporal)

captcha_workers_active

Reglas de alerta en Grafana

Un panel solo sirve si alguien lo mira; las alertas hacen el resto. Estas tres reglas cubren los fallos que de verdad frenan un pipeline: saldo bajo (LowBalance), tasa de error sobre el 10 % (HighErrorRate) y latencia p95 disparada (HighLatency). En la agencia del ejemplo, LowBalance evita que una campaña nocturna se pare por quedarse sin fondos.

# Grafana alert rules
groups:

  - name: captcha-alerts
    rules:

      - alert: LowBalance
        expr: captcha_balance_dollars < 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CaptchaAI balance low: {{ $value }}"

      - alert: HighErrorRate
        expr: |
          sum(rate(captcha_solves_total{status!="success"}[5m]))
          / sum(rate(captcha_solves_total[5m]))
          > 0.1
        for: 5m
        labels:
          severity: critical

      - alert: HighLatency
        expr: |
          histogram_quantile(0.95,
            rate(captcha_solve_duration_seconds_bucket[5m])
          ) > 120
        for: 10m
        labels:
          severity: warning

Resolución de problemas frecuentes

Problema Causa Solución
"Sin datos" en los paneles Prometheus no hace scrape del endpoint de métricas Revisa los targets de prometheus.yml y confirma que /metrics devuelve datos
Percentiles de latencia mal calculados Ventana de rate() inadecuada o buckets faltantes Usa la ventana [5m] y añade buckets más finos al histograma
Las variables del panel no responden Consulta de la variable de plantilla mal escrita Usa label_values(captcha_solves_total, captcha_type)
Las alertas no se disparan Intervalo de evaluación demasiado largo Baja el intervalo de evaluación a 1 minuto

Cómo importar estas plantillas en Grafana

Una vez que Prometheus recolecta las métricas, montar el panel lleva un par de minutos:

  1. En Grafana, añade tu Prometheus como fuente de datos.
  2. Crea un panel nuevo y pega cada consulta PromQL de la sección anterior.
  3. Asigna el tipo de panel indicado entre paréntesis (Stat, Gauge, serie temporal o gráfico circular).
  4. Exporta el panel terminado como JSON desde el menú de compartir.
  5. Importa ese JSON en tus demás instancias de Grafana para reutilizar la misma plantilla.

Así replicas exactamente el mismo panel en staging y en producción sin rehacerlo a mano, y lo versionas junto al resto de tu configuración.

Preguntas frecuentes

¿Qué buckets de latencia debo definir en el histograma?

Depende de tus tiempos de resolución. Los del ejemplo (de 5 a 300 segundos) cubren desde un Turnstile rápido hasta un reCAPTCHA v2 lento. Si casi todo se resuelve en menos de 30 segundos, añade buckets más finos para que p95 y p99 sean precisos.

¿Puedo vigilar varios tipos de CAPTCHA en el mismo panel?

Sí. La etiqueta captcha_type te permite segmentar por reCAPTCHA v2, reCAPTCHA v3, Turnstile o GeeTest v3. Usa by (captcha_type) en la serie temporal de la Fila 2 y compáralos lado a lado.

¿Cada cuánto debería hacer scrape Prometheus?

15 segundos es lo estándar. Para pipelines de menor volumen, 30 segundos ahorra almacenamiento. Baja de 10 solo si necesitas visibilidad casi en tiempo real: la cardinalidad crece rápido.

¿El saldo del panel afecta a mi plan de CaptchaAI?

No. El gauge de saldo solo refleja el crédito de tu cuenta; tu capacidad la fijan los threads de tu plan (por ejemplo, BASIC con $15/mes y 5 threads, o ADVANCE con $90/mes y 50 threads). LowBalance te avisa para recargar, no cambia tus threads.

Artículos relacionados

Próximos pasos

Deja de adivinar el estado de tu pipeline: obtén tu clave API de CaptchaAI e importa estos paneles de Grafana.

Guías relacionadas:

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