DevOps & Scaling

Мониторинг CaptchaAI с помощью New Relic: интеграция APM

Если пайплайн решения CAPTCHA деградирует ночью, а вы узнаёте об этом только от клиентов утром — логов уже недостаточно, нужен APM. New Relic превращает каждый вызов in.php/res.php в трейс с задержкой: у команд на нестабильных региональных сетях poll_count растёт раньше, чем падает success_rate. Ниже — инструментация на Python и Node.js, NRQL-запросы и пороги алертов.

Три сигнала, которые обычно показываются на дашборде раньше, чем клиенты напишут в поддержку:

  • Растёт poll_count, а success_rate ещё в норме. Первый спокойный признак — тормозит сеть или solver, а не то, что задачи массово проваливаются.
  • Растягивается P95 по времени решения. Обычно это предвестник таймаутов ещё до того, как сработает алерт по доле успешных решений.
  • Баланс тает без заметного роста трафика. Часто означает не рост нагрузки, а повторные попытки после ошибок — сам по себе повод завести отдельный алерт.

Какие метрики CAPTCHA-пайплайна важны для APM

[Submit Task] → [Wait for Solution] → [Apply Token]
     ↓                  ↓                   ↓
  Submit latency    Poll duration       Token usage
  API errors        Timeout rate        Success rate

Python: кастомные события New Relic для решения CAPTCHA

import os
import time
import requests
import newrelic.agent

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
session = requests.Session()


@newrelic.agent.background_task(name="captcha_solve", group="CaptchaAI")
def solve_captcha(sitekey, pageurl, captcha_type="recaptcha_v2"):
    """Solve a CAPTCHA with full New Relic instrumentation."""
    # Add custom attributes for filtering
    newrelic.agent.add_custom_attributes([
        ("captcha_type", captcha_type),
        ("target_url", pageurl),
    ])

    # Submit phase
    submit_result = _submit_task(sitekey, pageurl, captcha_type)
    if "error" in submit_result:
        newrelic.agent.record_custom_event("CaptchaSolveError", {
            "error": submit_result["error"],
            "phase": "submit",
            "captcha_type": captcha_type,
        })
        return submit_result

    # Poll phase
    captcha_id = submit_result["captcha_id"]
    poll_result = _poll_result(captcha_id, captcha_type)

    # Record solve event
    event_data = {
        "captcha_type": captcha_type,
        "captcha_id": captcha_id,
        "success": "solution" in poll_result,
    }
    if "solution" in poll_result:
        event_data["solve_time"] = poll_result.get("elapsed", 0)
        newrelic.agent.record_custom_event("CaptchaSolveSuccess", event_data)
    else:
        event_data["error"] = poll_result.get("error", "unknown")
        newrelic.agent.record_custom_event("CaptchaSolveError", event_data)

    return poll_result


@newrelic.agent.function_trace(name="captcha_submit")
def _submit_task(sitekey, pageurl, captcha_type):
    payload = {
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    }
    resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
    data = resp.json()

    newrelic.agent.add_custom_attributes([
        ("submit_status", data.get("status")),
    ])

    if data.get("status") != 1:
        return {"error": data.get("request")}
    return {"captcha_id": data["request"]}


@newrelic.agent.function_trace(name="captcha_poll")
def _poll_result(captcha_id, captcha_type):
    start = time.time()
    poll_count = 0

    for _ in range(60):
        time.sleep(5)
        poll_count += 1
        result = session.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": captcha_id, "json": 1
        }).json()

        if result.get("status") == 1:
            elapsed = time.time() - start
            newrelic.agent.add_custom_attributes([
                ("poll_count", poll_count),
                ("solve_time_seconds", round(elapsed, 2)),
            ])
            return {"solution": result["request"], "elapsed": elapsed}

        if result.get("request") != "CAPCHA_NOT_READY":
            return {"error": result.get("request")}

    return {"error": "TIMEOUT"}


def report_balance():
    """Record balance as a custom event."""
    resp = session.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance", "json": 1
    })
    data = resp.json()
    if data.get("status") == 1:
        balance = float(data["request"])
        newrelic.agent.record_custom_event("CaptchaBalance", {
            "balance": balance,
            "low": balance < 10,
        })
        return balance
    return None

Конфигурация агента New Relic

# newrelic.ini
[newrelic]
app_name = CaptchaAI Pipeline
license_key = YOUR_NEW_RELIC_LICENSE_KEY
monitor_mode = true
log_level = info
transaction_tracer.enabled = true
transaction_tracer.transaction_threshold = 5.0
custom_insights_events.enabled = true
custom_insights_events.max_samples_stored = 5000

Чек-лист перед первым деплоем агента

  • Сверьте license_key в newrelic.ini с нужным аккаунтом — несовпадающий ключ выглядит так, будто агент работает, а данные при этом летят не туда.
  • Держите transaction_tracer.transaction_threshold на уровне 1–2 секунд на тестовом контуре, иначе часть трейсов решения CAPTCHA просто не попадёт в трассировку.
  • Заранее прикиньте custom_insights_events.max_samples_stored под объём задач в минуту — при пиковой нагрузке лимит выбирается быстрее, чем кажется.
  • Прогоните newrelic-admin validate-config newrelic.ini перед первым деплоем: это быстрее, чем через час искать причину пустого дашборда.
  • Зафиксируйте базовый success_rate и solve_time за первые сутки — без такой базовой линии пороги алертов ниже выбираются на глаз.

JavaScript: трассировка решения CAPTCHA через New Relic

const newrelic = require("newrelic");
const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

async function solveCaptchaWithNewRelic(sitekey, pageurl, captchaType = "recaptcha_v2") {
  return newrelic.startBackgroundTransaction(
    "CaptchaSolve",
    "CaptchaAI",
    async () => {
      const transaction = newrelic.getTransaction();
      newrelic.addCustomAttributes({
        captchaType,
        targetUrl: pageurl,
      });

      const startTime = Date.now();

      try {
        // Submit
        const submitResp = await axios.post(
          "https://ocr.captchaai.com/in.php",
          null,
          {
            params: {
              key: API_KEY,
              method: "userrecaptcha",
              googlekey: sitekey,
              pageurl: pageurl,
              json: 1,
            },
          }
        );

        if (submitResp.data.status !== 1) {
          newrelic.recordCustomEvent("CaptchaSolveError", {
            error: submitResp.data.request,
            phase: "submit",
            captchaType,
          });
          transaction.end();
          return { error: submitResp.data.request };
        }

        const captchaId = submitResp.data.request;
        newrelic.addCustomAttributes({ captchaId });

        // Poll
        let pollCount = 0;
        for (let i = 0; i < 60; i++) {
          await new Promise((r) => setTimeout(r, 5000));
          pollCount++;

          const pollResp = await axios.get(
            "https://ocr.captchaai.com/res.php",
            {
              params: {
                key: API_KEY, action: "get", id: captchaId, json: 1,
              },
            }
          );

          if (pollResp.data.status === 1) {
            const elapsed = (Date.now() - startTime) / 1000;
            newrelic.recordCustomEvent("CaptchaSolveSuccess", {
              captchaType,
              solveTime: elapsed,
              pollCount,
            });
            newrelic.addCustomAttributes({
              solveTime: elapsed,
              pollCount,
            });
            transaction.end();
            return { solution: pollResp.data.request, elapsed };
          }

          if (pollResp.data.request !== "CAPCHA_NOT_READY") {
            newrelic.recordCustomEvent("CaptchaSolveError", {
              error: pollResp.data.request,
              phase: "poll",
              captchaType,
            });
            transaction.end();
            return { error: pollResp.data.request };
          }
        }

        newrelic.recordCustomEvent("CaptchaSolveError", {
          error: "TIMEOUT",
          phase: "poll",
          captchaType,
          pollCount,
        });
        transaction.end();
        return { error: "TIMEOUT" };
      } catch (err) {
        newrelic.noticeError(err);
        transaction.end();
        throw err;
      }
    }
  );
}

// Balance monitoring
async function monitorBalance() {
  try {
    const resp = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "getbalance", json: 1 },
    });
    if (resp.data.status === 1) {
      const balance = parseFloat(resp.data.request);
      newrelic.recordCustomEvent("CaptchaBalance", { balance });
    }
  } catch (err) {
    newrelic.noticeError(err);
  }
}

setInterval(monitorBalance, 60000);

module.exports = { solveCaptchaWithNewRelic };

Мониторинг для команд в СНГ и на нестабильных сетях

Команды, которые гоняют трафик через европейские регионы или разворачивают воркеров в Казахстане и Центральной Азии, чаще видят рост poll_count не из-за solver'а, а из-за самого канала до ocr.captchaai.com. Мобильные и спутниковые каналы добавляют джиттер, который на офисном десктопном интернете просто не заметен.

  • Разносите пороги алертов по региону, если разворачиваете несколько воркеров: единый порог для стабильного и нестабильного канала будет либо шуметь, либо молчать не вовремя.
  • Логируйте RTT до ocr.captchaai.com отдельным кастомным атрибутом — это отделяет сетевую деградацию от деградации самого solver'а прямо на дашборде.
  • Не сохраняйте в атрибутах New Relic больше персональных данных, чем нужно для отладки: для РФ это требование 152-ФЗ «О персональных данных», для международных команд — GDPR.

NRQL-запросы для дашборда

Соберите дашборд New Relic из этих запросов NRQL:

-- Solve success rate (last hour)
SELECT percentage(count(*), WHERE success = true)
FROM CaptchaSolveSuccess, CaptchaSolveError
SINCE 1 hour ago

-- Average solve time by CAPTCHA type
SELECT average(solveTime)
FROM CaptchaSolveSuccess
FACET captchaType
SINCE 1 hour ago TIMESERIES

-- Error breakdown
SELECT count(*)
FROM CaptchaSolveError
FACET error
SINCE 1 hour ago

-- P95 solve latency
SELECT percentile(solveTime, 95)
FROM CaptchaSolveSuccess
SINCE 1 hour ago TIMESERIES

-- Balance over time
SELECT latest(balance)
FROM CaptchaBalance
SINCE 24 hours ago TIMESERIES 5 minutes

-- Tasks per minute
SELECT rate(count(*), 1 minute)
FROM CaptchaSolveSuccess, CaptchaSolveError
SINCE 1 hour ago TIMESERIES

FAQ

Дашборд показывает рост poll_count, а success rate ещё в норме — уже проблема?

Да: решения приходят позже обычного из-за сети, а не потому что задачи проваливаются. Добавьте виджет average(pollCount) рядом с success_rate.

Custom events включены в newrelic.ini, но в NRQL пусто — что проверить сначала?

Не исчерпан ли max_samples_stored, и совпадает ли license_key с нужным аккаунтом.

Агент New Relic добавляет заметную задержку к решению CAPTCHA?

Нет: доли миллисекунды на вызов — на фоне секунд-минут решения это не измеримо.

Мониторинг из региона с нестабильной сетью — те же пороги алертов, что и для дата-центра?

Нет: для мобильных и региональных каналов держите отдельный порог high latency и не смешивайте его с трафиком из дата-центра — иначе алерт либо шумит на команду с нестабильной сетью, либо молчит на реальную деградацию у остальных.

Пороги для алертов

Откалибруйте пороги под конкурентность тарифа: команда на ADVANCE ($90/мес, 50 потоков) увидит совсем другой профиль нагрузки, чем на BASIC ($15/мес, 5 потоков). Ниже — стартовые значения, которые можно ужесточать по мере накопления истории.

Алерт Условие NRQL Порог
Низкая доля успешных решений SELECT percentage(count(*), WHERE success = true) < 85 % в течение 5 мин.
Высокая задержка SELECT percentile(solveTime, 95) FROM CaptchaSolveSuccess > 120 с в течение 10 мин.
Низкий баланс SELECT latest(balance) FROM CaptchaBalance < $10
Пик ошибок SELECT count(*) FROM CaptchaSolveError > 50 за 5 минут

Диагностика типичных проблем

Прежде чем разбираться в самом NRQL, проверьте четыре типичные причины тишины на дашборде — в большинстве случаев дело в конфигурации агента, а не в запросе.

Проблема Причина Решение
Кастомные события не появляются в NRQL custom_insights_events.enabled выключен Включите параметр в newrelic.ini
Трейсы транзакций отсутствуют Порог трассировки задан слишком высоким Понизьте transaction_threshold до 1.0 с
Атрибуты обрезаются Значение атрибута слишком длинное Держите значения атрибутов короче 255 символов
Нет данных после деплоя Неверный license key или агент не стартовал Проверьте newrelic-admin validate-config newrelic.ini

Следующие шаги

Дашборд из этого руководства закрывает основной цикл пайплайна: отправка → опрос → применение токена → баланс. Дальше стоит подключить смежные темы, которые чаще всего всплывают в первую неделю эксплуатации:

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