Tutorials

Данные временных рядов для CAPTCHA позволяют определить тенденции производительности

Если единственное, что вы знаете о своём решении CAPTCHA, — это «сейчас всё работает», о деградации вы узнаете от пользователей, а не от графика. Ряд наблюдений во времени отвечает на другой вопрос: что изменилось, когда именно и куда это движется. Ниже — рабочая схема метрик, код инструментирования на Python и JavaScript, запросы для Prometheus и InfluxDB и пороги, по которым имеет смысл ставить алерты.

Сбор метрик стоит подключать сразу, на первой интеграции — вместе с быстрым стартом CaptchaAI. Дописать инструментирование в работающий воркер дороже, чем заложить четыре счётчика в первый прототип.

Какие метрики решения CAPTCHA действительно нужны

Начните с минимального набора. Семь рядов ниже покрывают почти все инциденты, которые случаются с очередью решения на практике.

Метрика Тип Зачем
Доля успешных решений (%) Gauge Видно изменение качества на стороне провайдера
Задержка решения (мс) Histogram Замедления и подбор тайм-аутов
Частота ошибок по коду Counter Ранние признаки новой проблемы
Стоимость решения ($) Gauge Контроль бюджета, аномалии расхода
Глубина очереди Gauge Планирование числа потоков
Токены, истёкшие до использования Counter Сигнал к настройке TTL
Баланс аккаунта Gauge Триггер пополнения

Отдельно про стоимость. CaptchaAI тарифицируется по потокам, а не по числу решений: BASIC ($15/мес, 5 потоков), STANDARD ($30/мес, 15 потоков), ADVANCE ($90/мес, 50 потоков), PREMIUM ($170/мес, 100 потоков), CORPORATE ($240/мес, 150 потоков), ENTERPRISE ($300/мес, 200 потоков), а выше идут VIP-тарифы: 1 000 потоков — VIP-1 ($1,500/мес), 3 000 потоков — VIP-2 ($4,500/мес), 5 000 потоков — VIP-3 ($7,500/мес). Число решений внутри плана не ограничено, поэтому ряд «стоимость» полезен не как счёт к оплате, а как способ пересчитать фактическую цену одного решения при вашей загрузке.

Схема с Prometheus и Python

Prometheus — самый дешёвый вход, если он уже стоит в вашем контуре для инфраструктурных метрик. Воркер решения обычно живёт короткими процессами или задачами очереди, поэтому pull-модель к нему не подходит: пишем через Push Gateway.

Инструментируйте свой воркер

import os
import time
import requests
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge, push_to_gateway

registry = CollectorRegistry()

SOLVE_TOTAL = Counter(
    "captcha_solve_total", "Total CAPTCHA solve attempts",
    ["type", "status"], registry=registry
)
SOLVE_LATENCY = Histogram(
    "captcha_solve_latency_seconds", "CAPTCHA solve latency",
    ["type"], buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
    registry=registry
)
SOLVE_COST = Counter(
    "captcha_solve_cost_dollars", "Total cost of CAPTCHA solves",
    ["type"], registry=registry
)
API_BALANCE = Gauge(
    "captcha_api_balance_dollars", "CaptchaAI account balance",
    registry=registry
)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
PUSHGATEWAY = os.environ.get("PUSHGATEWAY_URL", "localhost:9091")


def solve_with_metrics(sitekey, pageurl, captcha_type="recaptcha_v2"):
    start = time.time()

    resp = requests.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:
        SOLVE_TOTAL.labels(type=captcha_type, status="submit_error").inc()
        push_metrics()
        return {"error": data.get("request")}

    captcha_id = data["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.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
            SOLVE_TOTAL.labels(type=captcha_type, status="solved").inc()
            SOLVE_LATENCY.labels(type=captcha_type).observe(elapsed)
            SOLVE_COST.labels(type=captcha_type).inc(0.00299)
            push_metrics()
            return {"solution": result["request"]}

        if result.get("request") != "CAPCHA_NOT_READY":
            SOLVE_TOTAL.labels(type=captcha_type, status="error").inc()
            push_metrics()
            return {"error": result.get("request")}

    SOLVE_TOTAL.labels(type=captcha_type, status="timeout").inc()
    push_metrics()
    return {"error": "TIMEOUT"}


def push_metrics():
    try:
        push_to_gateway(PUSHGATEWAY, job="captcha_solver", registry=registry)
    except Exception:
        pass  # Don't fail solving because metrics push failed


def update_balance():
    resp = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance"
    })
    try:
        balance = float(resp.text)
        API_BALANCE.set(balance)
        push_metrics()
    except ValueError:
        pass

Обратите внимание на push_metrics(): отправка метрик обёрнута в try/except и молча гасит исключения. Это сделано намеренно — недоступность Push Gateway не должна ронять решение CAPTCHA. Границы бакетов гистограммы (5–120 с) подобраны под реальное время решения, а не под обычные HTTP-запросы: бакеты по умолчанию дадут бесполезные перцентили.

Запросы PromQL

# Success rate over last hour
rate(captcha_solve_total{status="solved"}[1h])
/ rate(captcha_solve_total[1h]) * 100

# P95 solve latency
histogram_quantile(0.95, rate(captcha_solve_latency_seconds_bucket[1h]))

# Error rate by type
rate(captcha_solve_total{status="error"}[1h])

# Hourly cost
increase(captcha_solve_cost_dollars_total[1h])

Схема с InfluxDB

InfluxDB удобнее, когда метрики решения нужны как отдельное хранилище с высокой кардинальностью — например, если вы тегируете точки по типу CAPTCHA, коду ошибки и клиентскому проекту одновременно.

Запись показателей решения

from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS

INFLUX_URL = os.environ.get("INFLUX_URL", "http://localhost:8086")
INFLUX_TOKEN = os.environ.get("INFLUX_TOKEN", "")
INFLUX_ORG = os.environ.get("INFLUX_ORG", "captcha")
INFLUX_BUCKET = os.environ.get("INFLUX_BUCKET", "captcha_metrics")

influx_client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = influx_client.write_api(write_options=SYNCHRONOUS)


def record_solve_metric(captcha_type, status, elapsed_ms, cost=0.0, error=None):
    point = (
        Point("captcha_solve")
        .tag("type", captcha_type)
        .tag("status", status)
        .field("elapsed_ms", elapsed_ms)
        .field("cost", cost)
        .field("success", 1 if status == "solved" else 0)
    )
    if error:
        point = point.tag("error_code", error)
    write_api.write(bucket=INFLUX_BUCKET, record=point)


def record_balance(balance):
    point = Point("captcha_balance").field("balance", balance)
    write_api.write(bucket=INFLUX_BUCKET, record=point)

Запросы на Flux

// Success rate over last 24 hours (1-hour windows)
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "success")
  |> aggregateWindow(every: 1h, fn: mean)
  |> map(fn: (r) => ({r with _value: r._value * 100.0}))
  |> yield(name: "success_rate")

// Average solve time by type
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "elapsed_ms" and r.status == "solved")
  |> group(columns: ["type"])
  |> aggregateWindow(every: 1h, fn: mean)
  |> yield(name: "avg_latency")

// Cumulative cost
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "cost")
  |> cumulativeSum()
  |> yield(name: "cumulative_cost")

Реализация на JavaScript

Для Node.js-воркеров подход тот же: prom-client, те же имена метрик и те же границы бакетов, чтобы графики Python- и Node-воркеров складывались в один дашборд.

const client = require("prom-client");
const axios = require("axios");

const register = new client.Registry();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const solveTotal = new client.Counter({
  name: "captcha_solve_total",
  help: "Total CAPTCHA solve attempts",
  labelNames: ["type", "status"],
  registers: [register],
});

const solveLatency = new client.Histogram({
  name: "captcha_solve_latency_seconds",
  help: "CAPTCHA solve latency",
  labelNames: ["type"],
  buckets: [5, 10, 15, 20, 30, 45, 60, 90, 120],
  registers: [register],
});

async function solveWithMetrics(sitekey, pageurl, type = "recaptcha_v2") {
  const start = Date.now();

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

  if (submit.data.status !== 1) {
    solveTotal.inc({ type, status: "submit_error" });
    return { error: submit.data.request };
  }

  const captchaId = submit.data.request;

  for (let i = 0; i < 60; i++) {
    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: captchaId, json: 1 },
    });

    if (poll.data.status === 1) {
      const elapsed = (Date.now() - start) / 1000;
      solveTotal.inc({ type, status: "solved" });
      solveLatency.observe({ type }, elapsed);
      return { solution: poll.data.request };
    }

    if (poll.data.request !== "CAPCHA_NOT_READY") {
      solveTotal.inc({ type, status: "error" });
      return { error: poll.data.request };
    }
  }

  solveTotal.inc({ type, status: "timeout" });
  return { error: "TIMEOUT" };
}

// Expose metrics endpoint
const express = require("express");
const app = express();
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});
app.listen(9090);

Здесь метрики отдаются через HTTP-эндпоинт /metrics на порту 9090, то есть Prometheus забирает их сам. Push Gateway нужен только там, где процесс живёт меньше интервала опроса.

Как выбрать базу временных рядов

Особенность Prometheus InfluxDB TimescaleDB
Подходит для Оперативный мониторинг Метрики высокой кардинальности, IoT Аналитика на SQL
Язык запросов PromQL Flux SQL
Хранение По конфигурации По политике хранения Средствами PostgreSQL
Интеграция с Grafana Нативная Нативная Нативная
Порог входа Низкий Средний Низкий, если вы знаете SQL
Self-hosted Да Да Да, расширение PostgreSQL

Практическое правило: Prometheus, если он уже развёрнут; InfluxDB, если нужен изолированный сервис метрик; TimescaleDB, если аналитику по решениям будут строить люди, которые пишут SQL, а не PromQL.

Локальный сценарий: распределённые воркеры и разная сетевая задержка

Типичная конфигурация для команд, читающих по-русски: часть воркеров работает в европейском регионе облака, часть — на хостинге в Казахстане или Центральной Азии, а очередь общая. Без разбивки по географии график задержки покажет лишь смазанное среднее, и рост P95 на одной площадке утонет в общей картине.

Решение — добавить один низкокардинальный тег, например region, и сравнивать перцентили по площадкам. Время приёма-передачи (RTT) до API стоит вести отдельным рядом: тогда видно, растёт ли задержка из-за сети или из-за очереди. На нестабильных каналах именно это различие подсказывает, что чинить — тайм-ауты и повторные попытки на своей стороне.

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

Пороги алертов и типичные неисправности

Скользящие средние работают лучше мгновенных значений: одиночный сбой не должен будить дежурного. Разумные стартовые пороги — падение часовой доли успешных решений ниже 90 %, рост P95 задержки выше 45 с, а также обнуление глубины очереди при ненулевом входящем потоке.

Проблема Причина Что делать
Разрывы на графиках Push Gateway не получает данные Проверьте сеть между воркером и шлюзом и срок жизни процесса
Перцентили выглядят неправдоподобно Границы бакетов не соответствуют нагрузке Используйте бакеты [5, 10, 15, 20, 30, 45, 60, 90, 120] для решения CAPTCHA
Стоимость на графике не сходится с фактом Не учтена тарификация по потокам Считайте цену решения как стоимость плана, делённую на фактическое число решений за месяц
Слишком высокая кардинальность Много значений у меток Оставьте только type, status, error_code; не тегируйте по URL
Тайм-ауты растут только по одному типу Этот тип задачи требует больше времени Разделите SLO по типам: reCAPTCHA v2, Cloudflare Turnstile и GeeTest v3 ведут себя по-разному

Разбивка по типам особенно полезна, если вы решаете несколько семейств задач: reCAPTCHA v2, Cloudflare Turnstile и GeeTest v3 дают разные распределения времени решения, и общий график усредняет их до бесполезности.

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

Сколько хранить метрики решения CAPTCHA?

Посекундное разрешение — около 7 дней, часовые агрегаты — 90 дней, суточные сводки — бессрочно. Такой каскад даёт и разбор свежего инцидента, и годовой тренд без роста стоимости хранения.

Чем метрики отличаются от логов решений?

Метрики — это числовые ряды с низкой кардинальностью, по ним строят тренды и алерты. Логи хранят отдельные задачи с их ID и кодами ошибок и нужны для разбора конкретного случая. Держите оба контура, но не тегируйте метрики идентификаторами задач.

Что писать в ряд стоимости при тарификации по потокам?

Фиксируйте расчётную цену решения: месячную стоимость плана делите на фактическое число решений. Ряд покажет, как удельная цена падает с ростом загрузки — и в какой момент увеличение числа потоков окупается.

Нужно ли отдельно отслеживать баланс аккаунта?

Да, отдельным Gauge с редким опросом — раз в несколько минут достаточно. Это единственная метрика, которая ломает конвейер целиком, а не постепенно, поэтому алерт на неё ставят жёсткий, без сглаживания.

Какие метрики вести по бета-типам CAPTCHA?

CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) имеет смысл вести отдельными рядами и не смешивать с общими SLO: по бета-типам нет опубликованных измеренных показателей, поэтому опирайтесь на собственные наблюдения за период.


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

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