Tutorials

Journalisation structurée pour les opérations CAPTCHA

Une résolution CAPTCHA correctement instrumentée produit trois événements JSON au maximum — envoi, succès, échec — tous porteurs du même task_id. Avec ce schéma, retrouver pourquoi un parcours d'inscription a échoué hier à 14 h 12 tient dans une requête jq ; avec une ligne de texte brut du type Error solving captcha, cela demande une session de débogage complète.

La journalisation structurée ne change pas le volume écrit sur disque, mais sa forme : des champs typés, nommés à l'avance, que votre agrégateur sait filtrer et surveiller. Figez donc le schéma d'abord, puis instrumentez la résolution en Python et en Node.js.


Le schéma d'événement à figer avant d'écrire du code

Choisissez le nom des champs une fois pour toutes : un schéma stable vaut mieux qu'un schéma exhaustif. Un agrégateur ne sait rien faire d'un champ qui s'appelle solveTime un jour et solve_time_ms le lendemain.

Champ Type Rôle
event chaîne captcha_submitted, captcha_solved, captcha_solve_failed
task_id chaîne Identifiant de tâche CaptchaAI — la clé de corrélation
captcha_type chaîne recaptcha_v2, turnstile, image, etc.
site_url chaîne URL de la page cible
solve_time_ms entier Durée entre l'envoi et la réception du token
poll_attempts entier Nombre d'interrogations de res.php
error chaîne Code d'erreur renvoyé par l'API
token_length entier Longueur du token — jamais le token lui-même

Imposez deux conventions dès le départ : le suffixe _ms sur toute durée et le snake_case partout, y compris côté Node.js.


Texte brut ou JSON : la différence se voit à l'exploitation

Log en texte brut Log JSON structuré
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Analyse fragile, dépendante du format d'affichage Lisible par machine, champs typés
Recherche limitée au grep Filtrage et agrégation sur n'importe quel champ
Aucune corrélation entre les étapes Le task_id relie envoi, polling et injection

Instrumenter le cycle de résolution en Python avec structlog

structlog gère le rendu JSON et l'horodatage ISO ; vous ne manipulez que des paires clé-valeur.

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

Envoyer, interroger le résultat, tracer chaque issue

Tout tient dans le bind() : le contexte est attaché au logger une fois, puis chaque événement le transporte. Le sitekey est tronqué avant écriture, et la clé API n'apparaît dans aucun log.

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

Ce que vous obtenez sur la sortie standard :

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

Trois lignes, une résolution, un task_id commun : la chronologie se reconstitue sans jointure manuelle.


La même chaîne d'événements en Node.js avec pino

pino écrit du JSON par défaut et coûte peu en CPU, ce qui compte quand un worker traite des dizaines de résolutions en parallèle.

const pino = require('pino');

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

Attacher le contexte à un logger enfant

log.child() joue le rôle du bind() de structlog : un premier enfant porte le contexte de la page, un second y ajoute le 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;
}

Filtrer et alerter grâce à la journalisation structurée

Isoler les échecs de la dernière heure

Une fois le format figé, le filtrage devient trivial : jq suffit sur un fichier local ou dans un conteneur.

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

Surveiller le taux d'échec sur une fenêtre glissante

Un échec isolé n'a aucune valeur d'alerte. Ce qui compte, c'est la proportion d'échecs sur les cent dernières résolutions : au-delà de 20 %, quelque chose a changé côté site cible, proxys ou configuration.

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

Surveillez aussi poll_attempts : une médiane qui grimpe alors que le taux d'échec reste stable signale le plus souvent une saturation de vos threads, pas un problème de résolution. La facturation CaptchaAI reposant sur les threads, un plan BASIC ($15/mois, 5 threads) autorise cinq résolutions simultanées — la sixième attend son tour, et cette attente gonfle poll_attempts avant d'atteindre vos temps de réponse.


Logs de résolution et RGPD : ce qui ne doit pas atterrir sur disque

Vos logs CAPTCHA relèvent des mêmes obligations que le reste de votre télémétrie. Trois réflexes couvrent la majorité des cas :

  • N'écrivez jamais la clé API ni le token complet. Le champ token_length suffit au diagnostic.
  • Nettoyez site_url avant de la journaliser : une URL de formulaire contient souvent une adresse e-mail ou un identifiant client en paramètre. Gardez le chemin, supprimez la query string.
  • Fixez une durée de conservation et documentez-la. Trente à quatre-vingt-dix jours suffisent au diagnostic opérationnel.

Pour une équipe basée à Paris, Bruxelles ou Genève, garder ces événements dans une région européenne — eu-west-3, un serveur OVHcloud ou Scaleway — évite d'ouvrir un dossier de transfert hors UE pour une donnée aussi peu sensible qu'un task_id.

Côté volume, comptez trois lignes par résolution : 10 000 résolutions par jour font 30 000 événements. C'est le polling tracé ligne par ligne qui fait exploser la facture d'ingestion, jamais le cycle de vie.


Dépannage

Problème Cause probable Correctif
Volume de logs ingérable Un événement à chaque interrogation de res.php Ne tracez que l'envoi, le succès et l'échec
Événements impossibles à corréler task_id lié trop tard Liez-le dès la réponse de in.php avec log.bind() ou log.child()
Filtrage impossible dans l'agrégateur Texte brut, ou champs renommés d'un service à l'autre Passez à structlog ou pino et figez le schéma
Donnée sensible écrite en clair Clé API ou sitekey complet dans le contexte Tronquez le sitekey, n'injectez jamais YOUR_API_KEY
solve_time_ms incohérent Chronomètre démarré après l'appel d'envoi Mesurez avant la requête vers in.php

Questions fréquentes

Quels champs journaliser au minimum pour une résolution ?

Quatre suffisent : event, task_id, captcha_type et solve_time_ms. Ajoutez error et poll_attempts sur les échecs, et site_url si vous traitez plusieurs cibles.

Faut-il un identifiant de corrélation en plus du task_id ?

Oui, dans une application multi-étapes. Le task_id n'existe qu'après la réponse de in.php : liez aussi votre request_id ou trace_id dès l'entrée dans la fonction, sinon les erreurs d'envoi restent orphelines.

Comment mesurer le temps de résolution sans fausser la mesure ?

Démarrez le chronomètre juste avant l'appel d'envoi et arrêtez-le à la réception du token, polling compris. Ne mesurer que l'appel à res.php qui réussit donne un chiffre flatteur, inutile pour dimensionner vos timeouts.

Le polling doit-il apparaître dans les logs ?

Non, pas ligne par ligne. Un statut CAPCHA_NOT_READY est le comportement attendu : ne l'écrivez pas. Le compteur poll_attempts de l'événement final porte la même information pour une fraction du volume.

Combien de temps conserver les logs de résolution ?

Trente jours couvrent le débogage courant, quatre-vingt-dix jours l'analyse de tendance. Au-delà, agrégez en métriques — médiane, taux de réussite par type — et supprimez les événements bruts.


Rendez vos opérations CAPTCHA observables

Obtenez votre clé API sur captchaai.com et ajoutez vos trois premiers événements JSON : envoi, succès, échec.


Guides associés

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