DevOps & Scaling

Surveillance des taux de résolution de CAPTCHA avec Prometheus et Grafana

Un pipeline de résolution CAPTCHA se dégrade avant de tomber : le taux de réussite glisse, la durée P95 double, la file s'allonge — et le job nocturne rend des pages vides. Instrumentez trois séries — réussites sur tentatives, temps de résolution P95, solde du compte — et la dégradation devient visible avant l'incident.

Prometheus les collecte sur un endpoint /metrics exposé par votre worker, Grafana les affiche, une poignée de règles d'alerte vous préviennent avant vos utilisateurs. Voici la chaîne complète pour surveiller vos taux de résolution avec l'API CaptchaAI.


Six métriques qui décrivent votre taux de résolution

Chacune répond à une question précise.

Métrique Type Question
captcha_solves_total Compteur Volume et débit
captcha_solves_success Compteur Résolutions abouties
captcha_solves_errors Compteur Codes d'erreur dominants
captcha_solve_duration Histogramme Répartition des durées
captcha_balance Jauge Solde restant
captcha_queue_length Jauge Tâches en attente

Le compteur donne le volume, l'histogramme donne la forme : c'est la différence entre « le solveur est lent » et « seule la queue de distribution s'allonge ».


L'exporteur Python

L'instrumentation se place au plus près de l'appel réseau : compteur avant la soumission sur in.php, mesure de la durée, puis succès ou erreur après l'interrogation de res.php. Le label method porte le type de CAPTCHA.

# metrics.py
import time
import requests
from prometheus_client import (
    Counter, Histogram, Gauge, start_http_server,
)


# Define metrics
SOLVES_TOTAL = Counter(
    "captcha_solves_total",
    "Total CAPTCHA solve attempts",
    ["method"],
)

SOLVES_SUCCESS = Counter(
    "captcha_solves_success",
    "Successful CAPTCHA solves",
    ["method"],
)

SOLVES_ERRORS = Counter(
    "captcha_solves_errors",
    "Failed CAPTCHA solves",
    ["method", "error_code"],
)

SOLVE_DURATION = Histogram(
    "captcha_solve_duration_seconds",
    "CAPTCHA solve duration in seconds",
    ["method"],
    buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
)

BALANCE = Gauge(
    "captcha_balance_usd",
    "Current CaptchaAI account balance in USD",
)

QUEUE_LENGTH = Gauge(
    "captcha_queue_length",
    "Number of pending CAPTCHA tasks",
)


class InstrumentedSolver:
    """Solver with Prometheus metric instrumentation."""

    def __init__(self, api_key):
        self.api_key = api_key
        self.base = "https://ocr.captchaai.com"

    def solve(self, method, **params):
        """Solve CAPTCHA with metric collection."""
        SOLVES_TOTAL.labels(method=method).inc()
        start = time.time()

        try:
            token = self._do_solve(method, params)
            duration = time.time() - start

            SOLVES_SUCCESS.labels(method=method).inc()
            SOLVE_DURATION.labels(method=method).observe(duration)

            return token

        except Exception as e:
            error_code = str(e)[:30]
            SOLVES_ERRORS.labels(
                method=method, error_code=error_code,
            ).inc()
            raise

    def update_balance(self):
        """Fetch and update balance metric."""
        resp = requests.get(f"{self.base}/res.php", params={
            "key": self.api_key,
            "action": "getbalance",
            "json": 1,
        }, timeout=15)
        balance = float(resp.json()["request"])
        BALANCE.set(balance)
        return balance

    def _do_solve(self, method, params, timeout=120):
        data = {"key": self.api_key, "method": method, "json": 1}
        data.update(params)

        resp = requests.post(
            f"{self.base}/in.php", data=data, timeout=30,
        )
        result = resp.json()

        if result.get("status") != 1:
            raise RuntimeError(result.get("request"))

        task_id = result["request"]
        start = time.time()

        while time.time() - start < timeout:
            time.sleep(5)
            resp = requests.get(f"{self.base}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1,
            }, timeout=15)
            data = resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(data["request"])

        raise TimeoutError("Solve timeout")


# Start metrics server on port 8000
start_http_server(8000)
print("Metrics server running on :8000/metrics")

Attention à la cardinalité : error_code dérive ici du message d'exception tronqué. Si celui-ci contient un identifiant de tâche, vous créez une série temporelle par erreur. Normalisez sur les codes de l'API (CAPCHA_NOT_READY, ERROR_ZERO_BALANCE).


Configurer Prometheus

Une seule cible, collecte toutes les 10 secondes : descendre plus bas n'apporte rien sur des résolutions de plusieurs secondes.

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:

  - job_name: "captcha-solver"
    static_configs:

      - targets: ["solver-app:8000"]
    scrape_interval: 10s

La stack Docker Compose

Trois conteneurs : votre solveur, Prometheus, Grafana. La clé API passe par une variable d'environnement, jamais dans l'image ni dans un fichier versionné.

# docker-compose.yml
version: "3.8"

services:
  solver:
    build: .
    environment:

      - CAPTCHAAI_KEY=${CAPTCHAAI_KEY}
    ports:

      - "8000:8000"

  prometheus:
    image: prom/prometheus:latest
    volumes:

      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:

      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:

      - "3000:3000"
    environment:

      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:

      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

Piège classique : dans prometheus.yml, la cible est le nom de service Docker (solver-app) ; depuis ce conteneur, localhost désigne Prometheus lui-même.


Les requêtes PromQL du tableau de bord Grafana

Cinq panneaux couvrent l'essentiel ; ajoutez by (method) pour obtenir la même vue par type de CAPTCHA.

Taux de réussite

rate(captcha_solves_success[5m])
/ rate(captcha_solves_total[5m]) * 100

Temps de résolution moyen

rate(captcha_solve_duration_seconds_sum[5m])
/ rate(captcha_solve_duration_seconds_count[5m])

Erreurs par code

sum by (error_code) (
  rate(captcha_solves_errors[5m])
)

Solde du compte

captcha_balance_usd

Durée de résolution P95

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

Lisez la moyenne et la P95 ensemble : une moyenne stable avec une P95 qui grimpe signale une minorité de tâches en timeout, souvent sur un seul type de CAPTCHA.


Alerter avant que le taux de résolution ne s'effondre

Trois règles suffisent au départ : solde bas, taux d'erreur anormal, résolutions lentes. Les fenêtres for: absorbent les micro-pics.

# alert_rules.yml
groups:

  - name: captcha-alerts
    rules:

      - alert: LowBalance
        expr: captcha_balance_usd < 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CaptchaAI balance below $5"

      - alert: HighErrorRate
        expr: |
          rate(captcha_solves_errors[5m])
          / rate(captcha_solves_total[5m]) > 0.1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "CAPTCHA error rate above 10%"

      - alert: SlowSolveTime
        expr: |
          histogram_quantile(0.95,
            rate(captcha_solve_duration_seconds_bucket[5m])
          ) > 60
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "P95 solve time exceeds 60s"

Le seuil de solde à 5 dollars est un plancher : calez-le sur votre consommation réelle.


Seuils de résolution normaux, par type de CAPTCHA

Un seuil unique de 60 secondes convient à reCAPTCHA v2 mais reste aveugle pour Turnstile. Découpez vos règles avec le label method.

Les plafonds ci-dessous proviennent des pages solveur publiques de CaptchaAI. Les résultats varient selon l'environnement, le volume et le moment de la journée.

Type de CAPTCHA Plafond annoncé Seuil P95 de départ
Image / OCR < 0,5 s 3 s
Cloudflare Turnstile < 10 s 20 s
GeeTest v3 < 12 s 25 s
reCAPTCHA v2 < 60 s 90 s

Reprenez ensuite votre P95 réelle sur une période calme, puis ajoutez une marge.


Du tableau de bord au dimensionnement des threads

La facturation CaptchaAI se fait par thread concurrent, avec des résolutions illimitées par thread : la question n'est pas le volume du mois, mais le nombre de résolutions simultanées au pic.

Le calcul tient en une ligne : threads ≈ débit au pic × durée moyenne. Un worker à 30 résolutions par minute avec une durée moyenne de 10 s occupe environ 5 threads en continu, soit toute la capacité de BASIC ($15/mois, 5 threads) sans marge ; STANDARD ($30/mois, 15 threads) absorbe le pic, et ADVANCE ($90/mois, 50 threads) devient pertinent quand captcha_queue_length reste au-dessus de zéro alors que la durée par résolution ne bouge pas : une file qui s'allonge à durée constante signale des tâches en attente d'un thread libre.


Scénario : un pipeline de scraping hébergé chez OVHcloud

Une équipe QA lyonnaise exécute ses workers sur trois instances OVHcloud, avec un Prometheus mutualisé et une seule instance Grafana. Deux réglages ont tout changé.

D'abord, deux labels et pas un de plus : instance par machine, method par type. Ni URL cible, ni identifiant de compte testé : au-delà de la cardinalité, ces valeurs finissent dupliquées dans une base que personne ne purge — exactement ce que le RGPD vous demande d'éviter.

Ensuite, un seuil distinct pour le job nocturne, dont la P95 est structurellement plus haute : SlowSolveTime sonnait chaque nuit à 2 h. Une alerte ignorée par habitude ne protège plus rien.


Dépannage

Problème Cause probable Correctif
/metrics répond vide start_http_server(8000) jamais appelé Démarrez le serveur dans le processus qui résout
Cible « down » dans Prometheus Mauvais hôte ou port Visez le nom de service Docker, pas localhost
Grafana sans données Source Prometheus non déclarée Ajoutez http://prometheus:9090, puis rechargez
Compteurs à zéro après un déploiement Redémarrage du worker, attendu Interrogez rate(), jamais le compteur brut

FAQ

Pourquoi le taux de réussite chute-t-il aux heures de pointe ?

Le plus souvent, ce sont les threads et non le solveur : si captcha_queue_length grossit alors que la durée par résolution reste stable, vos tâches attendent un thread libre.

À quelle fréquence interroger le solde du compte ?

Une fois par minute suffit. Appelez update_balance() depuis une boucle dédiée : la jauge répond ensuite aux scrapes sans appel supplémentaire.

Quel seuil P95 choisir pour l'alerte de lenteur ?

Un seuil par type. Les 60 s du fichier d'alertes conviennent à reCAPTCHA v2 ; sur Cloudflare Turnstile, dont le plafond annoncé est inférieur à 10 s, une P95 au-delà de 20 s mérite un coup d'œil.

Combien de temps faut-il conserver ces métriques ?

Quinze jours couvrent le diagnostic courant : c'est la rétention par défaut de Prometheus. Pour comparer d'un mois sur l'autre, exportez vos agrégats vers un stockage longue durée.

Puis-je suivre hCaptcha ou FunCaptcha avec ces métriques ?

Non — hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge par CaptchaAI. Le label method couvre reCAPTCHA v2 et v3, Cloudflare Turnstile et Challenge, GeeTest v3, l'image/OCR, la grille et BLS, plus CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta). GeeTest v4 est annoncé comme à venir.


Guides connexes


Vos taux de résolution méritent mieux qu'un fichier de logs : ouvrez un compte CaptchaAI et branchez votre premier /metrics.

Les commentaires sont désactivés pour cet article.