Un auditeur SOC 2, ou une demande d'accès au titre du RGPD, pose toujours la même question : pouvez-vous prouver qui a déclenché chaque résolution de CAPTCHA, pour quel site et à quel coût ? Sans piste d'audit, la réponse n'existe pas. Un journal d'audit enregistre chaque demande envoyée à CaptchaAI — déclencheur, horodatage, résultat et temps de résolution — pour rendre chaque appel traçable et rattachable à une opération précise. Ce guide montre comment construire cette journalisation en Python et en Node.js, l'interroger, puis la conserver selon votre volume.
Ce que doit contenir un journal d'audit CAPTCHA
Une piste d'audit exploitable ne se résume pas à « résolu / échoué » : elle doit permettre de reconstituer, des mois plus tard, le contexte exact d'une résolution. Enregistrez au minimum les champs suivants :
| Champ | Objectif | Exemple |
|---|---|---|
timestamp |
Quand la demande a été faite | 2026-04-04T14:30:00Z |
request_id |
Identifiant unique pour cette résolution | uuid4() |
captcha_type |
Méthode CAPTCHA utilisée | userrecaptcha |
target_site |
URL de la page en cours de résolution | https://example.com/login |
task_id |
ID de tâche CaptchaAI | 73829451 |
status |
Résultat | solved, failed, timeout |
solve_time_ms |
Temps entre la soumission et le résultat | 18432 |
error_code |
Erreur en cas d'échec | ERROR_CAPTCHA_UNSOLVABLE |
initiator |
Qui ou quoi a déclenché la résolution | scraper-job-42 |
cost |
Coût estimé | 0.003 |
À ne jamais journaliser, sous peine d'alourdir votre exposition RGPD :
- vos clés API ;
- les tokens CAPTCHA, temporaires et sans valeur d'audit une fois expirés ;
- toute donnée personnelle issue des sites cibles : adresse e-mail, contenu de formulaire, identifiant de compte.
Ce dernier point relève directement du RGPD : le principe de minimisation impose de ne conserver que ce qui sert réellement à l'audit. Un initiator et un task_id suffisent pour la traçabilité ; l'adresse e-mail ou le contenu d'un formulaire, non.
Implémentation en Python
Le point clé est d'isoler la journalisation d'audit de vos logs applicatifs : un fichier dédié au format JSON Lines (une ligne JSON par résolution), avec rotation automatique. Le RotatingFileHandler de la bibliothèque standard suffit, et un enregistrement est écrit qu'une résolution réussisse, échoue ou expire.
# audit_solver.py
import os
import uuid
import time
import json
import logging
from datetime import datetime, timezone
import requests
API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")
# Configure audit logger — separate from application logs
audit_logger = logging.getLogger("captcha_audit")
audit_logger.setLevel(logging.INFO)
# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
"captcha_audit.jsonl",
maxBytes=50_000_000, # 50 MB per file
backupCount=10,
)
handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(handler)
def log_audit(record):
"""Write a structured audit record."""
audit_logger.info(json.dumps(record, default=str))
def solve_with_audit(sitekey, pageurl, captcha_type="userrecaptcha",
initiator="unknown"):
"""Solve a CAPTCHA with full audit logging."""
request_id = str(uuid.uuid4())
start = time.time()
audit_record = {
"request_id": request_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"captcha_type": captcha_type,
"target_site": pageurl,
"initiator": initiator,
"status": "submitted",
}
session = requests.Session()
try:
# Submit
resp = session.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": captcha_type,
"googlekey": sitekey,
"pageurl": pageurl,
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
audit_record.update({
"status": "submit_failed",
"error_code": result.get("request"),
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
task_id = result["request"]
audit_record["task_id"] = task_id
# Poll
time.sleep(15)
for _ in range(25):
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
})
poll_result = poll.json()
if poll_result.get("status") == 1:
solve_time = int((time.time() - start) * 1000)
audit_record.update({
"status": "solved",
"solve_time_ms": solve_time,
"cost_estimate": 0.003, # Adjust per your rate
})
log_audit(audit_record)
return poll_result["request"]
if poll_result.get("request") != "CAPCHA_NOT_READY":
audit_record.update({
"status": "failed",
"error_code": poll_result.get("request"),
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
time.sleep(5)
audit_record.update({
"status": "timeout",
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
except Exception as e:
audit_record.update({
"status": "error",
"error_code": str(e)[:200],
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
raise
# Usage
token = solve_with_audit(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://www.google.com/recaptcha/api2/demo",
initiator="price-scraper-v2",
)
Format de sortie JSONL
Chaque résolution produit une ligne autonome, facile à filtrer avec grep, à ingérer dans un agrégateur ou à charger dans un tableur. Exemple d'une résolution réussie :
{"request_id":"a1b2c3d4-...","timestamp":"2026-04-04T14:30:00+00:00","captcha_type":"userrecaptcha","target_site":"https://www.google.com/recaptcha/api2/demo","initiator":"price-scraper-v2","status":"solved","task_id":"73829451","solve_time_ms":18432,"cost_estimate":0.003}
Implémentation en JavaScript (Node.js)
La même logique se transpose dans une pile Node.js : fs.appendFileSync ajoute une ligne par résolution ; pour un fort débit, remplacez-le par un flux d'écriture (fs.createWriteStream) afin d'éviter les écritures synchrones bloquantes.
// audit_solver.js
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
const AUDIT_FILE = 'captcha_audit.jsonl';
function logAudit(record) {
fs.appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n');
}
async function solveWithAudit(sitekey, pageurl, initiator = 'unknown') {
const requestId = uuidv4();
const start = Date.now();
const record = {
request_id: requestId,
timestamp: new Date().toISOString(),
captcha_type: 'userrecaptcha',
target_site: pageurl,
initiator,
status: 'submitted',
};
try {
const submit = await axios.get('https://ocr.captchaai.com/in.php', {
params: {
key: API_KEY, method: 'userrecaptcha',
googlekey: sitekey, pageurl, json: '1',
},
});
if (submit.data.status !== 1) {
record.status = 'submit_failed';
record.error_code = submit.data.request;
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
}
record.task_id = submit.data.request;
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 25; i++) {
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
});
if (poll.data.status === 1) {
record.status = 'solved';
record.solve_time_ms = Date.now() - start;
record.cost_estimate = 0.003;
logAudit(record);
return poll.data.request;
}
if (poll.data.request !== 'CAPCHA_NOT_READY') {
record.status = 'failed';
record.error_code = poll.data.request;
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
}
await new Promise(r => setTimeout(r, 5000));
}
record.status = 'timeout';
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
} catch (e) {
record.status = 'error';
record.error_code = e.message.slice(0, 200);
record.solve_time_ms = Date.now() - start;
logAudit(record);
throw e;
}
}
Interroger vos journaux d'audit CAPTCHA
Un journal n'a de valeur que si vous pouvez le lire. Comme chaque ligne est un objet JSON, quelques lignes de Python suffisent à produire le récapitulatif qu'un responsable conformité attend :
- le volume de résolutions du jour ;
- la répartition des statuts (
solved,failed,timeout) ; - le coût estimé cumulé ;
- le temps de résolution médian.
Résumé quotidien
import json
from collections import Counter
from datetime import date
def daily_summary(log_file, target_date=None):
"""Generate a daily summary from audit logs."""
target = target_date or date.today().isoformat()
statuses = Counter()
total_cost = 0
solve_times = []
with open(log_file) as f:
for line in f:
record = json.loads(line)
if record["timestamp"].startswith(target):
statuses[record["status"]] += 1
total_cost += record.get("cost_estimate", 0)
if record.get("solve_time_ms"):
solve_times.append(record["solve_time_ms"])
print(f"Date: {target}")
print(f"Total requests: {sum(statuses.values())}")
print(f"Statuses: {dict(statuses)}")
print(f"Estimated cost: ${total_cost:.2f}")
if solve_times:
print(f"Median solve time: {sorted(solve_times)[len(solve_times)//2]}ms")
daily_summary("captcha_audit.jsonl")
Ce récapitulatif sert aussi de contrôle croisé : ses totaux de coût doivent correspondre à ceux de votre tableau de bord CaptchaAI. Un écart signale une résolution non journalisée ou une erreur d'estimation du coût unitaire.
Rétention et stockage
Le volume du journal augmente linéairement avec le nombre de résolutions. Ne conservez pas tout indéfiniment : dimensionnez le stockage selon votre débit réel et purgez selon une durée définie à l'avance.
| Volume | Taille du journal quotidien | Stockage mensuel | Recommandation |
|---|---|---|---|
| 100 résolutions/jour | ~30 Ko | ~1 Mo | Fichier local |
| 1 000 résolutions/jour | ~300 Ko | ~10 Mo | Fichier local + rotation |
| 10 000 résolutions/jour | ~3 Mo | ~100 Mo | Expédier vers un agrégateur de logs |
| 100 000 résolutions/jour | ~30 Mo | ~1 Go | Journalisation centralisée (ELK, Datadog) |
Pour une équipe basée en Europe, expédier ces journaux vers une région comme eu-west-3 (Paris) garde les données près de vos autres traitements et simplifie l'argumentaire de résidence des données en audit.
Trois règles évitent les dérives de rétention :
- fixez une durée de conservation avant de commencer à journaliser, pas après coup ;
- automatisez la purge (
logrotate, politique de cycle de vie du bucket) plutôt que de la déclencher à la main ; - chiffrez les journaux au repos dès que le débit justifie un stockage centralisé.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Le fichier journal devient trop volumineux | Aucune rotation configurée | Utilisez RotatingFileHandler ou logrotate |
| Des enregistrements d'audit manquent | Exception levée avant l'écriture | Journalisez dans un bloc finally |
| Écritures lentes à fort volume | Entrées/sorties fichier synchrones | Passez aux écritures asynchrones ou à un tampon |
| Horodatages incohérents | Dérive de l'horloge système | Synchronisez via NTP ; journalisez en UTC |
FAQ
Le RGPD s'applique-t-il à un journal d'audit de résolution CAPTCHA ?
Cela dépend de son contenu. Un journal limité au task_id, au statut et à l'initiator technique ne contient pas de donnée personnelle. Il y tombe dès qu'il enregistre une adresse IP, un identifiant de compte ou le contenu d'un formulaire — d'où la règle de ne journaliser que les champs nécessaires à la traçabilité.
Faut-il séparer les journaux d'audit des logs applicatifs ?
Oui. Un journal d'audit a une finalité, un format et une durée de conservation propres. Le mélanger à vos logs de debug complique les recherches en audit et rend la purge sélective quasi impossible. Utilisez un fichier ou un flux dédié, comme ci-dessus.
Comment relier une résolution à l'opération qui l'a déclenchée ?
Renseignez le champ initiator à chaque appel — nom du job, identifiant de worker ou version de script (price-scraper-v2). C'est lui qui transforme une liste de résolutions anonymes en piste d'audit traçable, capable de répondre à « qui a lancé ceci ? ».
Puis-je rapprocher ces journaux de ma facturation CaptchaAI ?
Oui. La facturation CaptchaAI repose sur le nombre de threads simultanés — par exemple BASIC ($15/mois, 5 threads) — et non sur le nombre de résolutions. Vos journaux ne remplacent pas la facture, mais ils aident à vérifier le débit réel et à ventiler le coût par job ou par équipe.
Combien de temps faut-il conserver ces journaux ?
90 jours est une durée courante pour des journaux opérationnels. Pour une journalisation orientée conformité (SOC 2, RGPD, HIPAA), alignez la rétention sur les exigences de votre secteur et documentez votre règle de purge.
Articles connexes
Prochaines étapes
Ajoutez de la traçabilité à chaque résolution de CAPTCHA — récupérez votre clé API CaptchaAI.
Guides associés :
- la surveillance de votre tableau de bord d'utilisation
- la journalisation structurée des opérations CAPTCHA
- la vérification du solde et la recharge automatique