DevOps & Scaling

Мониторинг CaptchaAI с помощью Datadog: метрики и оповещения

Пайплайн решения CAPTCHA обычно ломается тихо: баланс API незаметно уходит в ноль, воркер зависает, задержка растёт — а вы узнаёте об этом из отчёта поддержки, а не из алерта. Datadog закрывает этот разрыв: семь метрик пайплайна на одном дашборде и оповещения, которые срабатывают раньше, чем клиенты успевают пожаловаться.

Какие метрики CaptchaAI стоит собирать

Метрика Тип Зачем нужна
captcha.solve.count Счётчик Сколько задач отправлено всего
captcha.solve.success Счётчик Сколько решений получено успешно
captcha.solve.error Счётчик Сколько решений упало, в разбивке по типу ошибки
captcha.solve.latency Гистограмма Время от отправки задачи до готового решения
captcha.queue.depth Датчик Сколько задач сейчас ждёт в очереди
captcha.balance Датчик Остаток баланса API
captcha.worker.active Датчик Сколько воркеров активно прямо сейчас

Счётчики (Counter) считают события нарастающим итогом, датчики (Gauge) показывают текущее значение на момент снятия, а гистограмма (Histogram) даёт распределение задержки — из неё Datadog строит перцентили p50/p95/p99 для дашборда ниже.

Python: интеграция через DogStatsD

Ниже — декоратор, который оборачивает вызов решения reCAPTCHA v2 и сам отправляет счётчики и гистограмму задержки в локальный агент DogStatsD:

import os
import time
import functools
import requests
from datadog import initialize, statsd

# Initialize Datadog
initialize(
    statsd_host=os.environ.get("DD_AGENT_HOST", "localhost"),
    statsd_port=int(os.environ.get("DD_DOGSTATSD_PORT", "8125"))
)

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


def track_captcha_metrics(captcha_type="recaptcha_v2"):
    """Decorator to track solve metrics."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            tags = [f"captcha_type:{captcha_type}"]
            statsd.increment("captcha.solve.count", tags=tags)

            start = time.time()
            try:
                result = func(*args, **kwargs)
                elapsed = time.time() - start

                if "solution" in result:
                    statsd.increment("captcha.solve.success", tags=tags)
                    statsd.histogram("captcha.solve.latency", elapsed, tags=tags)
                else:
                    error = result.get("error", "unknown")
                    statsd.increment(
                        "captcha.solve.error",
                        tags=tags + [f"error:{error}"]
                    )
                return result
            except Exception as e:
                statsd.increment(
                    "captcha.solve.error",
                    tags=tags + [f"error:{type(e).__name__}"]
                )
                raise
        return wrapper
    return decorator


@track_captcha_metrics(captcha_type="recaptcha_v2")
def solve_recaptcha(sitekey, pageurl):
    resp = session.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    })
    data = resp.json()
    if data.get("status") != 1:
        return {"error": data.get("request")}

    captcha_id = data["request"]
    for _ in range(60):
        time.sleep(5)
        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:
            return {"solution": result["request"]}
        if result.get("request") != "CAPCHA_NOT_READY":
            return {"error": result.get("request")}
    return {"error": "TIMEOUT"}


def report_balance():
    """Send balance as a gauge metric."""
    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"])
        statsd.gauge("captcha.balance", balance)
        return balance
    return None


def report_queue_depth(depth):
    """Report current queue depth."""
    statsd.gauge("captcha.queue.depth", depth)


def report_worker_count(active, total):
    """Report worker health."""
    statsd.gauge("captcha.worker.active", active)
    statsd.gauge("captcha.worker.total", total)

JavaScript: та же схема через hot-shots

Та же логика на Node.js — обёртка solveCaptchaWithMetrics шлёт счётчики и гистограмму через клиент hot-shots, а фоновый таймер каждую минуту репортит баланс:

const { StatsD } = require("hot-shots");
const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

const dogstatsd = new StatsD({
  host: process.env.DD_AGENT_HOST || "localhost",
  port: parseInt(process.env.DD_DOGSTATSD_PORT || "8125", 10),
  prefix: "captcha.",
  globalTags: [`env:${process.env.NODE_ENV || "development"}`],
});

async function solveCaptchaWithMetrics(sitekey, pageurl, captchaType = "recaptcha_v2") {
  const tags = [`captcha_type:${captchaType}`];
  dogstatsd.increment("solve.count", 1, tags);
  const startTime = Date.now();

  try {
    const result = await solveCaptcha(sitekey, pageurl);
    const elapsed = (Date.now() - startTime) / 1000;

    if (result.solution) {
      dogstatsd.increment("solve.success", 1, tags);
      dogstatsd.histogram("solve.latency", elapsed, tags);
    } else {
      dogstatsd.increment("solve.error", 1, [...tags, `error:${result.error}`]);
    }

    return result;
  } catch (err) {
    dogstatsd.increment("solve.error", 1, [...tags, `error:${err.message}`]);
    throw err;
  }
}

async function solveCaptcha(sitekey, pageurl) {
  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) {
    return { error: submitResp.data.request };
  }

  const captchaId = submitResp.data.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    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) return { solution: pollResp.data.request };
    if (pollResp.data.request !== "CAPCHA_NOT_READY") {
      return { error: pollResp.data.request };
    }
  }
  return { error: "TIMEOUT" };
}

async function reportBalance() {
  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);
      dogstatsd.gauge("balance", balance);
      return balance;
    }
  } catch (err) {
    console.error("Balance check failed:", err.message);
  }
  return null;
}

// Report balance every minute
setInterval(reportBalance, 60000);

module.exports = { solveCaptchaWithMetrics, reportBalance };

Готовый дашборд Datadog в JSON

Не собирайте виджеты вручную — импортируйте этот JSON как есть, и панель мониторинга CAPTCHA появится за минуту:

{
  "title": "CaptchaAI Pipeline",
  "widgets": [
    {
      "definition": {
        "type": "timeseries",
        "title": "Solve Rate (Success vs Error)",
        "requests": [
          {"q": "sum:captcha.solve.success{*}.as_count()"},
          {"q": "sum:captcha.solve.error{*}.as_count()"}
        ]
      }
    },
    {
      "definition": {
        "type": "timeseries",
        "title": "Solve Latency (p50, p95, p99)",
        "requests": [
          {"q": "avg:captcha.solve.latency{*}"},
          {"q": "percentile:captcha.solve.latency{*},0.95"},
          {"q": "percentile:captcha.solve.latency{*},0.99"}
        ]
      }
    },
    {
      "definition": {
        "type": "query_value",
        "title": "API Balance",
        "requests": [{"q": "avg:captcha.balance{*}"}]
      }
    },
    {
      "definition": {
        "type": "timeseries",
        "title": "Queue Depth",
        "requests": [{"q": "avg:captcha.queue.depth{*}"}]
      }
    }
  ]
}

Четыре виджета покрывают минимум для дежурного: график успехов и ошибок в динамике, перцентили задержки p50/p95/p99, текущий баланс API одним числом и глубина очереди — этого достаточно, чтобы за пять секунд понять, деградирует пайплайн или нет, не открывая логи.

Какие алерты настроить в первую очередь

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

Алерт Условие Серьёзность
Низкий баланс captcha.balance < 10 Предупреждение
Критический баланс captcha.balance < 2 Критический
Высокая доля ошибок Доля ошибок > 10% за 5 минут Предупреждение
Скачок задержки Задержка p95 > 120 с в течение 10 минут Предупреждение
Очередь растёт Глубина очереди > 100 и продолжает расти 5 минут подряд Предупреждение
Воркер недоступен captcha.worker.active == 0 Критический
# Datadog monitor definition (API create)
- type: metric alert
  name: "CaptchaAI Low Balance"
  query: "avg(last_5m):avg:captcha.balance{*} < 10"
  message: "CaptchaAI balance is low: {{value}}. Top up to avoid solve failures."
  tags:

    - team:scraping
    - service:captcha

Устранение неполадок

Прежде чем разбираться с каждой метрикой отдельно, проверьте базовые вещи — почти все проблемы мониторинга сводятся к четырём причинам ниже.

Проблема Причина Решение
Метрики не появляются в Datadog Агент DogStatsD не запущен Проверьте переменную DD_AGENT_HOST; убедитесь, что контейнер агента виден в docker ps
Гистограмма задержки пустая Ни одно успешное решение не было зафиксировано Проверьте, что statsd.histogram() вызывается именно в ветке успеха, а не только при ошибке
Теги не отображаются Неверный формат тега Используйте формат key:value, без пробелов внутри тега
Метрики дублируются Одновременно работает несколько репортеров Оставьте один процесс, который отправляет баланс, на весь деплой — иначе гистограммы и суммы задвоятся

Частые вопросы

Нужен ли DogStatsD-агент на каждом воркере?

Нет, достаточно одного агента на хост. Все воркеры этого хоста отправляют метрики на локальный агент (DD_AGENT_HOST), а он уже сам агрегирует и пересылает их в Datadog.

Как выбрать порог алерта на баланс, чтобы не поймать простой в пиковую нагрузку?

Считайте порог от расхода в час, а не от произвольного числа. Если у вас, например, ADVANCE (50 потоков) и вечерний пик трафика из СНГ после 19:00 по МСК, порог captcha.balance < 10 должен покрывать хотя бы час работы на этой скорости — иначе алерт придёт, когда пополнить баланс уже не успеть до конца пика.

Можно ли одновременно собирать метрики CaptchaAI в Datadog и в Prometheus/Grafana?

Да, конфликта нет: statsd.increment() и statsd.gauge() из примеров выше просто шлют метрики в DogStatsD, а Prometheus-экспортер можно повесить рядом на том же коде — источник данных один, разница только в том, куда его отправлять и в какой системе строить графики.

Нужно ли исключать персональные данные из тегов метрик?

Да. В тегах captcha_type, error, env держите только технические значения — не кладите туда IP-адреса, cookies или другие данные пользователей. Это не только гигиена мониторинга, но и разумная предосторожность, если ваша команда обрабатывает персональные данные по 152-ФЗ или GDPR: собирайте и логируйте только то, что действительно нужно для эксплуатации.

С чего начать, если раньше в проекте вообще не было метрик?

Не пытайтесь сразу покрыть всё. На первой итерации хватит четырёх счётчиков (solve.count, solve.success, solve.error, balance) и одного алерта на низкий баланс — этого уже достаточно, чтобы не пропустить самый частый инцидент: пайплайн молча встал, потому что закончились потоки. Гистограмму задержки, дашборд и алерт на очередь добавляйте вторым шагом, когда появится история данных за пару недель и будет с чем сравнивать всплески.


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

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