DevOps & Scaling

Мониторинг скорости решения CAPTCHA с помощью Prometheus и Grafana

Конвейер решения CAPTCHA может «просесть» посреди ночи: вырастет время ответа, начнёт распухать очередь задач, баланс тихо уйдёт в ноль — а вы узнаете об этом только утром, из жалоб пользователей. Поймать проблему раньше них можно только одним способом: снимать метрики с воркера в реальном времени. Prometheus собирает эти метрики, Grafana превращает их в дашборд, а правила оповещений будят дежурного до того, как упадёт конверсия.


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

Шести метрик достаточно для первого дашборда: три счётчика дают долю успешных решений и разбивку ошибок, гистограмма — распределение времени ответа, а два датчика — состояние очереди и баланса.

Метрика Тип Что показывает
captcha_solves_total Счётчик Всего попыток решения
captcha_solves_success Счётчик Успешные решения
captcha_solves_errors Счётчик Неудачные решения (по типу ошибки)
captcha_solve_duration Гистограмма Распределение времени решения
captcha_balance Датчик Текущий остаток на счёте
captcha_queue_length Датчик Задачи, ожидающие в очереди

Остальное — пропускная способность по типам CAPTCHA, латентность до ocr.captchaai.com — добавляйте по мере роста нагрузки, начинать стоит именно с этих шести.


Экспортер метрик на Python

Ниже — минимальный экспортер: обёртка над вызовами in.php/res.php, которая пишет значения в prometheus_client и поднимает HTTP-сервер /metrics на порту 8000. Дальше Prometheus сам приходит и опрашивает этот порт по расписанию.

# metrics.py
import time
import requests
from prometheus_client import (
    Counter, Histogram, Gauge, start_http_server,
)


# Define metrics
SOLVES_TOTAL = Counter(
    "captcha_solves_total",
    "Total CAPTCHA solve attempts",
    ["method"],
)

SOLVES_SUCCESS = Counter(
    "captcha_solves_success",
    "Successful CAPTCHA solves",
    ["method"],
)

SOLVES_ERRORS = Counter(
    "captcha_solves_errors",
    "Failed CAPTCHA solves",
    ["method", "error_code"],
)

SOLVE_DURATION = Histogram(
    "captcha_solve_duration_seconds",
    "CAPTCHA solve duration in seconds",
    ["method"],
    buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
)

BALANCE = Gauge(
    "captcha_balance_usd",
    "Current CaptchaAI account balance in USD",
)

QUEUE_LENGTH = Gauge(
    "captcha_queue_length",
    "Number of pending CAPTCHA tasks",
)


class InstrumentedSolver:
    """Solver with Prometheus metric instrumentation."""

    def __init__(self, api_key):
        self.api_key = api_key
        self.base = "https://ocr.captchaai.com"

    def solve(self, method, **params):
        """Solve CAPTCHA with metric collection."""
        SOLVES_TOTAL.labels(method=method).inc()
        start = time.time()

        try:
            token = self._do_solve(method, params)
            duration = time.time() - start

            SOLVES_SUCCESS.labels(method=method).inc()
            SOLVE_DURATION.labels(method=method).observe(duration)

            return token

        except Exception as e:
            error_code = str(e)[:30]
            SOLVES_ERRORS.labels(
                method=method, error_code=error_code,
            ).inc()
            raise

    def update_balance(self):
        """Fetch and update balance metric."""
        resp = requests.get(f"{self.base}/res.php", params={
            "key": self.api_key,
            "action": "getbalance",
            "json": 1,
        }, timeout=15)
        balance = float(resp.json()["request"])
        BALANCE.set(balance)
        return balance

    def _do_solve(self, method, params, timeout=120):
        data = {"key": self.api_key, "method": method, "json": 1}
        data.update(params)

        resp = requests.post(
            f"{self.base}/in.php", data=data, timeout=30,
        )
        result = resp.json()

        if result.get("status") != 1:
            raise RuntimeError(result.get("request"))

        task_id = result["request"]
        start = time.time()

        while time.time() - start < timeout:
            time.sleep(5)
            resp = requests.get(f"{self.base}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1,
            }, timeout=15)
            data = resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(data["request"])

        raise TimeoutError("Solve timeout")


# Start metrics server on port 8000
start_http_server(8000)
print("Metrics server running on :8000/metrics")

Обратите внимание: _do_solve считает и успешные, и неуспешные попытки. Это важно, чтобы captcha_solves_total действительно был знаменателем для доли успешных решений, а не только суммой удачных вызовов.


Конфигурация Prometheus

Укажите адрес воркера в scrape_configs — Prometheus подключится к порту 8000 и будет опрашивать /metrics раз в 10 секунд:

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:

  - job_name: "captcha-solver"
    static_configs:

      - targets: ["solver-app:8000"]
    scrape_interval: 10s

Docker Compose стек

Для локальной проверки или staging удобнее поднять весь стек одной командой — воркер, Prometheus и Grafana в одном docker-compose-файле:

# docker-compose.yml
version: "3.8"

services:
  solver:
    build: .
    environment:

      - CAPTCHAAI_KEY=${CAPTCHAAI_KEY}
    ports:

      - "8000:8000"

  prometheus:
    image: prom/prometheus:latest
    volumes:

      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:

      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:

      - "3000:3000"
    environment:

      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:

      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

После docker compose up Grafana доступна на :3000 (логин и пароль по умолчанию — admin/admin), Prometheus — на :9090.


Запросы для дашборда Grafana

Из этих пяти PromQL-запросов собирается стартовый дашборд.

Доля успешных решений (PromQL)

rate(captcha_solves_success[5m])
/ rate(captcha_solves_total[5m]) * 100

Среднее время решения

rate(captcha_solve_duration_seconds_sum[5m])
/ rate(captcha_solve_duration_seconds_count[5m])

Ошибки по типу

sum by (error_code) (
  rate(captcha_solves_errors[5m])
)

Баланс во времени

captcha_balance_usd

Время решения, перцентиль P95

histogram_quantile(0.95,
  rate(captcha_solve_duration_seconds_bucket[5m])
)

Правила оповещений

Три правила закрывают базовый SLA:

  • заканчивающийся баланс — предупреждение, пока есть время пополнить счёт;
  • всплеск ошибок — сигнал о проблеме на стороне сети, прокси или самой CAPTCHA;
  • деградация времени ответа — ранний признак того, что P95 подбирается к пользовательскому таймауту.
# alert_rules.yml
groups:

  - name: captcha-alerts
    rules:

      - alert: LowBalance
        expr: captcha_balance_usd < 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CaptchaAI balance below $5"

      - alert: HighErrorRate
        expr: |
          rate(captcha_solves_errors[5m])
          / rate(captcha_solves_total[5m]) > 0.1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "CAPTCHA error rate above 10%"

      - alert: SlowSolveTime
        expr: |
          histogram_quantile(0.95,
            rate(captcha_solve_duration_seconds_bucket[5m])
          ) > 60
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "P95 solve time exceeds 60s"

При интеграции с Alertmanager эти же правила легко направить не только на email, но и в Telegram или Slack — команды, работающие в вечерних и ночных часовых поясах СНГ, обычно заводят именно туда, чтобы не пропустить инцидент до утра.


Практический сценарий: распределённая команда

Команда, разнесённая между Алматы, Минском и Белградом, обслуживает парсер, который решает reCAPTCHA v2 и Cloudflare Turnstile через API CaptchaAI. Воркеры развёрнуты в европейском облачном регионе — так латентность до ocr.captchaai.com предсказуема и не зависит от того, из какого города дежурит инженер в конкретную ночь. В такой схеме датчик captcha_queue_length обычно становится первым сигналом: если очередь растёт быстрее, чем убывает баланс, не хватает потоков, а не денег, и решение — поднять план, а не паниковать.

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

Что стоит проверить перед тем, как объявлять дашборд готовым:

  • метки method/error_code не содержат значений с высокой кардинальностью (например, сырых ID задач);
  • дашборд открывается и без доступа к конкретному воркеру — по общим меткам, а не по имени хоста;
  • у каждого правила оповещения есть понятное дежурному описание причины, а не только номер ошибки.

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

Большинство инцидентов с этим стеком сводятся к четырём причинам — ниже они собраны в одну таблицу, чтобы не искать их по логам вручную.

Проблема Причина Решение
На /metrics нет данных Сервер не запущен Вызовите start_http_server(8000)
Prometheus показывает target как down Неверный адрес или порт Проверьте сеть и порт контейнера в Docker
В Grafana нет данных Prometheus не добавлен как источник данных Добавьте data source Prometheus в Grafana
Метрики обнуляются при перезапуске Ожидаемое поведение — счётчик сбрасывается Используйте rate(), а не «сырые» значения счётчиков

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

Насколько сильно Prometheus замедляет воркер?

Практически никак: prometheus_client добавляет заметно меньше 1 мс на операцию с метрикой, а опрос раз в 10–15 секунд не создаёт ощутимой нагрузки даже на небольшом VPS.

Как мониторить несколько API-ключей CaptchaAI одновременно?

Добавьте метку key (или account) к каждой метрике и запускайте отдельный экспортер на аккаунт — либо один процесс с несколькими инстансами InstrumentedSolver. В Grafana достаточно группировать запросы по этой метке: агрегация по всем ключам считается автоматически.

Как отправлять оповещения в Telegram, а не только на email?

Настройте у Alertmanager ресивер webhook и подключите к нему Telegram-бот-шлюз (например, alertmanager-telegram-bot). Сами правила из раздела про оповещения не меняются — меняется только то, куда Alertmanager доставляет уведомление.

Сколько хранить историю метрик?

Локальный Prometheus по умолчанию хранит данные около 15 дней — этого достаточно для разбора конкретного инцидента. Для долгосрочных трендов (сравнение месяц к месяцу) подключите remote_write в Grafana Cloud, Mimir или VictoriaMetrics; конфигурация самого экспортера при этом не меняется.

С каких порогов начать в правилах оповещений?

Значения из примера в этой статье — рабочая отправная точка, а не догма: баланс ниже $5, доля ошибок выше 10 % за 10 минут и P95 времени решения выше 60 секунд. Подстройте их под свой трафик через одну-две недели наблюдения за реальными графиками, а не сразу «под ноль» — слишком чувствительные пороги быстро превращаются в шум, который дежурный инженер начинает игнорировать.


Связанные материалы


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

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