Tutorials

Структурированное журналирование операций CAPTCHA

Структурированный лог решения CAPTCHA — это одна JSON-строка на каждое значимое событие задачи: отправку, получение токена, ошибку, тайм-аут. В строке есть task_id, тип задачи, время решения и код ошибки, поэтому вопрос «почему ночью просела доля успешных решений» закрывается фильтром по одному полю, а не чтением текстового файла глазами.

Ниже — рабочая схема для Python (structlog) и Node.js (pino): какие события писать, какие поля обязательны для корреляции, как выбрать сбои через jq и как повесить оповещение на долю ошибок. Код показан на интеграции с API CaptchaAI: отправка задачи в in.php, опрос res.php, возврат токена.


Какие события писать в лог

Полезный набор событий у задачи решения CAPTCHA небольшой, и расширять его почти никогда не нужно:

  1. captcha_submit_start — запрос сформирован, тип задачи и целевая страница уже известны.
  2. captcha_submitted — задача принята, появился task_id; фиксируем время отправки.
  3. captcha_solved — токен получен; пишем время решения и число запросов опроса.
  4. captcha_solve_failed — сервис вернул код ошибки, он же попадает в поле error.
  5. captcha_solve_timeout — окно опроса закончилось без результата.

Отдельная запись на каждую итерацию опроса — самая частая причина того, что логи становятся бесполезными: почти весь объём занимает ожидаемое состояние CAPCHA_NOT_READY, а редкие настоящие ошибки в нём тонут.


Обычный текст и JSON: в чём разница

Строка в текстовом логе Событие в JSON
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Разбирается только регулярными выражениями Читается любым парсером и коллектором
Поиск сводится к grep по подстроке Фильтр по любому полю: типу, коду ошибки, длительности
События одной задачи ничем не связаны task_id сшивает отправку, опрос и подстановку токена

Python: настройка structlog

Конфигурация занимает несколько строк: временная метка в формате ISO, уровень события и рендер в JSON.

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

Жизненный цикл задачи в логах

Ключевой приём — log.bind(): контекст задачи (тип, адрес страницы, обрезанный sitekey) привязывается один раз, а task_id добавляется к тому же логгеру сразу после ответа in.php. Дальше каждое событие несёт полный набор полей, и передавать их руками уже не нужно.

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

Здесь важны две детали. API-ключ не попадает в лог ни разу, а sitekey обрезан до двенадцати символов: этого достаточно, чтобы отличить один стенд от другого, и недостаточно, чтобы вытащить из логов чужую конфигурацию. Токен тоже не пишется целиком — в событии остаётся только token_length.

Что попадает в вывод:

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

Node.js: pino и дочерние логгеры

В Node.js та же схема строится на log.child(): дочерний логгер наследует поля родителя, поэтому контекст задачи не приходится собирать заново в каждом вызове.

const pino = require('pino');

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

Тот же жизненный цикл на Node.js

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;
}

Если сервис написан сразу на двух языках, договоритесь об общих именах полей до того, как логи поедут в коллектор. solve_time_ms в Python и solveTimeMs в Node.js — это два разных поля для любого дашборда, и сводить их потом дороже, чем один раз принять соглашение.


Справочник полей события

Поле Тип Что означает
event строка Имя события: captcha_submitted, captcha_solved и другие
task_id строка ID задачи CaptchaAI — по нему сшиваются все записи
captcha_type строка recaptcha_v2, turnstile, image и другие
site_url строка Адрес целевой страницы
solve_time_ms целое Время от отправки задачи до готового токена
poll_attempts целое Сколько запросов опроса потребовалось
error строка Код ошибки, возвращённый API
token_length целое Длина полученного токена

Фильтрация, поиск и оповещения

Найти все сбои за последний час

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

Предупредить о росте доли ошибок

Скользящее окно последних задач надёжнее суточных сводок: деградация на конкретном типе задач видна за минуты, а не на следующее утро.

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

Как связать логи с расходом потоков

Тарификация CaptchaAI построена на одновременных потоках: BASIC ($15/мес, 5 потоков), STANDARD ($30/мес, 15 потоков), ADVANCE ($90/мес, 50 потоков). Логи — самый дешёвый способ понять, упирается ли пайплайн в число потоков или в само время решения.

Практический пример: команда из Алматы гоняет ночные интеграционные тесты на европейском регионе облака, и ночью solve_time_ms вырастает с 15 000 до 40 000 мс. Дальше решают два других поля. Если вместе с ним пропорционально растёт poll_attempts, задача действительно дольше решается — это вопрос к параллелизму воркеров и сетевым задержкам. Если же растёт submit_ms, а в поле error появляется ERROR_NO_SLOT_AVAILABLE, упор идёт в число одновременных потоков: задачи не принимаются, пока не освободится слот. Первый случай лечится расписанием прогонов, второй — тарифом с большим числом потоков. Цены зафиксированы в USD, поэтому расчёт одинаково читается для распределённой команды.

Отдельный вопрос — содержимое логов. Если рядом с событиями задачи пишутся данные из тестируемой формы, это уже персональные данные со всеми требованиями 152-ФЗ и сопоставимых норм для трансграничных команд. Практичное правило: в лог попадают только технические поля из таблицы выше, а значения формы маскируются на уровне процессора логгера.


Частые проблемы и что с ними делать

Симптом Причина Решение
Логов слишком много Пишется каждая итерация опроса Оставить отправку, успех, ошибку и тайм-аут
События не связываются между собой Нет общего идентификатора Привязать task_id через log.bind() или log.child() сразу после ответа in.php
По логам невозможно искать Формат — свободный текст Перейти на JSON: structlog в Python, pino в Node.js
В логах видны секреты Пишется полный API-ключ Не логировать ключ, sitekey обрезать, токен заменять его длиной
Дашборд не собирается из двух сервисов Разные имена полей Зафиксировать единую схему полей до подключения коллектора

Часто задаваемые вопросы

Как связать отправку и получение токена, если задач тысячи в час?

По task_id из ответа in.php. Привяжите его к логгеру сразу после отправки — тогда выборка по одному идентификатору покажет всю историю задачи: отправку, число запросов опроса и итог.

Что нельзя писать в логи решения CAPTCHA?

API-ключ, полный токен и данные из тестируемой формы. Токен раздувает объём (500+ символов на запись), а данные формы переводят логи в категорию персональных данных. Технических полей из справочника выше хватает для разбора инцидента.

Сколько хранить такие логи и какой объём они занимают?

Одно событие в JSON — примерно 200–300 байт, то есть четыре события на задачу дают около 1 КБ. При 50 000 задач в сутки это порядка 50 МБ в день. Рабочая практика: 7–14 дней в горячем хранилище для разбора инцидентов и агрегаты (доля ошибок, медиана solve_time_ms) на длинном горизонте.

Как отличить рост времени решения от роста доли ошибок?

Стройте два ряда из одних и тех же событий: медиану solve_time_ms по успешным задачам и долю captcha_solve_failed от общего числа. Растёт только первая метрика — вопрос к параллелизму и сетевым задержкам; растёт вторая, особенно с повторяющимся кодом в поле error, — вопрос к самой интеграции и к типу задачи.

Нужен ли отдельный лог под каждый тип задачи?

Нет, достаточно поля captcha_type в общем потоке событий. Разделять индексы имеет смысл только тогда, когда нагрузка по типам различается на порядки и мешает выборкам по редкому типу.


Соберите наблюдаемые процессы решения CAPTCHA на CaptchaAI

Получите API-ключ на сайте CaptchaAI и подключите логи по схеме выше.


Смежные руководства

Комментарии для этой статьи отключены.