Сколько стоил вчерашний трафик CAPTCHA и кто его запустил — скрапер, воркер очереди или разработчик, тестирующий интеграцию? Без отдельного журнала аудита ответ теряется уже через сутки. Если команда решает через CaptchaAI тысячи CAPTCHA в день, структурированный журнал — базовое требование для комплаенса, разбора инцидентов и сверки биллинга, а не опция.
Дальше — обязательные поля, код на Python и Node.js и разбор накопленных логов.
Зачем вести журнал аудита отдельно от лога приложения
Обычный лог приложения фиксирует ошибки и отладочные сообщения, но редко хранит контекст, нужный для комплаенса или разбора биллинга. Отдельный журнал аудита решает три задачи, с которыми обычный лог не справляется:
- Атрибуция запроса. Каждая запись привязана к источнику —
scraper-job-42,qa-checkout-suiteили конкретному сервису, а не просто к строке в общем логе. - Сверка стоимости. Поле
costв каждой записи позволяет сопоставить журнал с расходом потоков в панели управления CaptchaAI, не дожидаясь конца расчётного периода. - Разбор инцидентов. Когда решение падает по таймауту или возвращает
ERROR_CAPTCHA_UNSOLVABLE, журнал восстанавливает хронологию без обращения в поддержку.
Для агентств и интеграторов, которые обслуживают несколько проектов через один API-ключ, отдельный журнал — ещё и способ показать клиенту фактическое потребление по его задачам, а не просто общий счёт за поток.
Обязательные поля журнала аудита
Каждое решение фиксирует:
| Поле | Назначение | Пример |
|---|---|---|
timestamp |
Время запроса | 2026-04-04T14:30:00Z |
request_id |
ID решения | uuid4() |
captcha_type |
Тип CAPTCHA | userrecaptcha |
target_site |
Адрес страницы проверки | https://staging.example.com/qa-login |
task_id |
ID задачи CaptchaAI | 73829451 |
status |
Итог | solved, failed, timeout |
solve_time_ms |
Время решения | 18432 |
error_code |
Код ошибки | ERROR_CAPTCHA_UNSOLVABLE |
initiator |
Источник запроса | scraper-job-42 |
cost |
Стоимость | 0.003 |
Не логируйте: API-ключи, токены CAPTCHA и персональные данные с целевых сайтов.
Python: аудит-логирование решения
Обёртка над in.php/res.php: пишет JSON-строку на каждом этапе — отправке, опросе, итоге:
Совет: выносите запись аудит-лога в отдельный поток, очередь или буфер. Синхронная запись на диск в основном обработчике заметно замедляет решение при высокой нагрузке — подробнее в разделе «Типичные проблемы» ниже.
# 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",
)
Формат записи (JSONL)
Каждая строка — самостоятельный JSON-объект, его проще разбирать построчно, чем массив:
{"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}
Node.js-версия
Та же логика на JavaScript — те же поля и шаги:
// 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;
}
}
Как анализировать накопленные логи
Дневная сводка
Скрипт считает решения по статусам и стоимость — этого хватит для быстрой проверки:
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")
На что обращать внимание в сводке
- Рост доли
failed/timeoutотносительноsolved— сигнал деградации апстрима, смены разметки целевой страницы или проблем с прокси. - Медиана
solve_time_ms, растущая день ко дню, — повод проверить нагрузку на очередь или сменившийся тип CAPTCHA. - Всплеск
costбез роста числаsolved— обычно значит, что растёт доля неудачных попыток, за которые вы всё равно платите.
Объём журналов и хранение
Размер растёт линейно с объёмом решений:
| Объём | В день | В месяц | Рекомендация |
|---|---|---|---|
| 100 решений/день | ~30 КБ | ~1 МБ | Локальный файл |
| 1000 решений/день | ~300 КБ | ~10 МБ | Файл + ротация |
| 10 000 решений/день | ~3 МБ | ~100 МБ | Агрегатор логов |
| 100 000 решений/день | ~30 МБ | ~1 ГБ | ELK / Datadog |
Типичные проблемы
| Проблема | Причина | Исправление |
|---|---|---|
| Файл журнала разрастается | Нет ротации | RotatingFileHandler или logrotate |
| Записи аудита пропадают | Исключение до записи | Пишите запись в блоке finally |
| Медленная запись при нагрузке | Синхронный файловый I/O | Асинхронная запись или буферизация |
| Метки времени не совпадают | Рассинхронизация часов | NTP; время только в UTC |
Часто задаваемые вопросы
Нужно ли учитывать 152-ФЗ или GDPR при хранении журналов аудита?
Обычно нет — журнал не содержит персональных данных, если вы не логируете токены и содержимое страниц. Если target_site или initiator связаны с данными пользователей, сверьтесь с 152-ФЗ или GDPR.
Стоит ли сохранять сам токен CAPTCHA в аудит-логе?
Нет. Токен живёт 60–300 секунд и не имеет ценности постфактум.
Можно ли сверять журнал аудита с балансом CaptchaAI?
Да. Сравнивайте стоимость из логов с расходом потоков в панели управления — это ловит расхождения в биллинге раньше.
Что делать, если аудит нужен нескольким командам?
Используйте initiator как идентификатор источника (scraper-job-42, qa-checkout-suite) и фильтруйте сводки по нему.
Нужно ли шифровать файл журнала аудита на диске?
Если запись не содержит токенов и персональных данных, шифрование не обязательно. Но ограничьте доступ к файлу правами ОС, держите ротацию включённой и не копируйте captcha_audit.jsonl в общедоступные бэкапы без проверки прав доступа — это снижает риск при утечке снапшота диска.