DevOps & Scaling

Modelos de painel Grafana para métricas CaptchaAI

Quando a fila de CAPTCHA trava às três da manhã, a primeira pergunta do plantão é sempre a mesma: a taxa de resolução caiu ou é só a fila que cresceu? Um painel Grafana bem montado responde isso em segundos. Esta página traz quatro linhas de painel prontas para importar - visão geral, desempenho, erros e workers - usando o Prometheus como fonte de dados, para que você não precise desenhar cada gráfico do zero.

Pré-requisitos antes de importar os painéis

Antes de colar as consultas abaixo, confirme três coisas no seu ambiente:

  • O processo que chama a API da CaptchaAI já expõe um endpoint /metrics (os exemplos em Python e Node.js abaixo mostram como instrumentar isso).
  • O Prometheus está configurado para raspar (scrape) esse endpoint - adicione o alvo em prometheus.yml.
  • O Grafana já tem o Prometheus cadastrado como fonte de dados (data source), local ou via Grafana Cloud.

Resolvidos esses três pontos, o resto é colar PromQL em painéis novos.

Como organizar o layout do painel

Divida o painel em quatro linhas, cada uma respondendo a uma pergunta diferente do plantão: "está tudo bem?" (visão geral), "está lento?" (desempenho), "está falhando?" (erros) e "tenho capacidade sobrando?" (workers). Essa ordem também funciona bem como ordem de leitura durante um incidente - de cima para baixo, do sintoma à causa.

┌───────────────────────────────────────────────┐
│ 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]           │
└───────────────────────────────────────────────┘

Exponha métricas Prometheus no seu solver CAPTCHA

Antes de qualquer painel funcionar, o seu pipeline CAPTCHA precisa publicar as métricas. Os dois exemplos abaixo - Python e Node.js - instrumentam a mesma chamada à API da CaptchaAI e expõem um contador de resoluções, um histograma de latência, o saldo da conta, a profundidade da fila e o número de workers ativos.

Python: cliente Prometheus para o pipeline CAPTCHA

O trecho a seguir envolve a chamada de resolve com métricas e sobe um servidor HTTP na porta 9090 para o Prometheus raspar:

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)

O Counter de resoluções carrega dois labels - captcha_type e status - o que já deixa o painel de erros (linha 3) pronto para segmentar por tipo de CAPTCHA sem nenhuma consulta extra.

Node.js: cliente prom-client

Mesma lógica, agora expondo /metrics via Express:

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

Se os seus workers rodam em uma região como o AWS sa-east-1 (São Paulo), vale abrir um painel de RTT separado: a latência de rede até o endpoint da CaptchaAI se soma ao tempo de resolução mostrado nos percentis, e comparar regiões sem isolar essa parcela costuma gerar conclusões erradas sobre desempenho.

Consultas PromQL para as quatro linhas do painel

Com as métricas expostas, cole estas consultas diretamente nos painéis correspondentes.

Linha 1: estatísticas gerais

Esses quatro painéis cabem numa única linha e respondem à pergunta de triagem inicial sem exigir nenhum drill-down:

  • Taxa de resolução (painel de estatísticas)
sum(rate(captcha_solves_total{status="success"}[5m]))
/
sum(rate(captcha_solves_total[5m]))

* 100
  • Saldo (painel de medidor)
captcha_balance_dollars
  • Profundidade da fila (painel de estatísticas)
captcha_queue_depth
  • Tarefas por minuto (painel de estatísticas)
sum(rate(captcha_solves_total[5m])) * 60

Linha 2: desempenho

Se o p95 subir enquanto o p50 continua estável, o sintoma costuma ser um subconjunto de tipos de CAPTCHA mais lentos, não uma degradação geral - segmentar por captcha_type confirma rápido:

  • Percentis de latência (série 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]))
  • Taxa de resolução ao longo do tempo (série temporal)
sum(rate(captcha_solves_total{status="success"}[5m])) by (captcha_type) * 60

Linha 3: erros

O detalhamento por status separa error de timeout - um pico de timeout costuma apontar para o intervalo de polling, enquanto um pico de error aponta para a chamada de submissão em si:

  • Taxa de erro (série temporal)
sum(rate(captcha_solves_total{status!="success"}[5m]))
/
sum(rate(captcha_solves_total[5m]))

* 100
  • Detalhamento de erros (gráfico de pizza)
sum by (status) (increase(captcha_solves_total{status!="success"}[1h]))

Linha 4: workers

Cruze esse painel com a profundidade da fila da linha 1: fila crescendo com workers estáveis é sinal de falta de paralelismo, não de solver lento.

  • Workers ativos (série temporal)
captcha_workers_active

Regras de alerta no Grafana

# 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

Ajuste os três limites ao seu volume real antes de ativar em produção: o 10 do saldo mínimo, o 0.1 (10%) da taxa de erro e o 120 segundos de latência p95 são pontos de partida, não valores fixos. Ao escrever anotações de alerta (o campo summary), evite incluir sitekeys de produção, tokens ou identificadores de usuários reais - trate esses campos como dado sensível sob a LGPD (ou o RGPD, em Portugal) e prefira valores agregados.

Solução de problemas comuns

Problema Causa provável Correção
"Sem dados" nos painéis Prometheus não está raspando /metrics Confira os targets em prometheus.yml
Percentis de latência estranhos Janela rate() errada ou buckets grosseiros Use [5m]; refine os buckets do histograma
Variáveis do painel não funcionam Consulta de template incorreta Use label_values(captcha_solves_total, captcha_type)
Alertas não disparam Intervalo de avaliação longo demais Reduza o evaluation_interval para 1 minuto
Saldo aparece zerado com créditos na conta update_balance() roda só sob demanda Rode a atualização de saldo em um job periódico

Perguntas frequentes

Como faço um único painel mostrar cada tipo de CAPTCHA separadamente?

Use uma variável de template ligada a label_values(captcha_solves_total, captcha_type) e aplique o filtro {captcha_type="$captcha_type"} nas consultas PromQL desta página. Assim você troca de reCAPTCHA v2 para Turnstile no mesmo painel sem duplicar gráficos.

Que valor de threshold faz sentido para o alerta de saldo baixo?

Depende do seu consumo diário médio. Uma regra prática é definir o threshold como o equivalente a 24-48 horas de gasto no ritmo atual, para dar tempo de recarregar antes que o saldo zere e as resoluções comecem a falhar por ERROR_ZERO_BALANCE.

Qual intervalo de scrape do Prometheus faz sentido para esse pipeline?

15 segundos é o padrão seguro para a maioria dos casos. Em pipelines de volume baixo, 30 segundos já é suficiente e reduz a carga de armazenamento. Evite descer abaixo de 10 segundos, a menos que você realmente precise de granularidade quase em tempo real para debugar um incidente.

Preciso hospedar o Prometheus para esses painéis funcionarem?

Não necessariamente. O Grafana Cloud aceita Prometheus remote write, então você continua rodando o prometheus_client ou o prom-client no seu processo, mas envia as séries para uma instância gerenciada em vez de manter o Prometheus em produção. As consultas PromQL desta página funcionam do mesmo jeito nos dois cenários.

Qual plano da CaptchaAI é suficiente para alimentar esse painel de métricas?

A instrumentação em si não consome créditos - as métricas vêm do seu próprio processo, não de uma chamada extra à API. O que importa é o plano que já cobre o seu volume de resolução: para pipelines pequenos, BASIC (US$ 15/mês, 5 threads) costuma bastar, já que a cobrança é por thread concorrente com resoluções ilimitadas por thread, não por CAPTCHA resolvido.

Depois de montar os painéis

Com o pipeline instrumentado, obtenha sua chave de API da CaptchaAI, aponte o Prometheus para o endpoint de métricas e importe as quatro linhas de painel descritas acima.

Leitura complementar:

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