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_lengthsuffit au diagnostic. - Nettoyez
site_urlavant 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.