Отдельный дашборд мониторинга — лишняя сущность, если команда и так живёт в Discord. Подключите статус конвейера CAPTCHA к серверу Discord через webhook — и баланс, всплески ошибок, глубина очереди и суточная сводка будут приходить прямо в рабочий канал готовыми embed-сообщениями, без стороннего сервиса оповещений.
Вебхук — самый простой способ достичь этого: не нужно регистрировать Discord-приложение, настраивать OAuth и держать отдельный процесс-бота онлайн. Одна переменная окружения с URL и HTTP-запрос из вашего скрипта — это весь интеграционный контур.
Как выглядят сообщения в Discord
Прежде чем разворачивать вебхук, вот как готовые embed-сообщения выглядят в канале — именно это увидит дежурный, когда конвейер начнёт слать оповещения:
Оповещение о балансе:
💰 Оповещение о балансе Баланс CaptchaAI составляет $8,42 Порог: $10,00 | Серьёзность: ПРЕДУПРЕЖДЕНИЕ
Всплеск ошибок:
⚠️ Скачок частоты ошибок Частота ошибок: 22,5% (45/200) •
ERROR_CAPTCHA_UNSOLVABLE: 30 •TIMEOUT: 15
Суточная сводка:
📈 Ежедневная сводка CAPTCHA 12 450 обработанных задач Доля успешных решений: 95,2% | Средняя задержка: 22,4 с | Баланс: $142,30
Настройка вебхука в Discord
- Откройте настройки вашего сервера Discord.
- Перейдите в раздел Интеграции → Вебхуки.
- Нажмите Новый вебхук и назовите его «CaptchaAI Alerts».
- Скопируйте URL вебхука.
- Сохраните его в переменной окружения
DISCORD_WEBHOOK_URL.
Один вебхук на канал закрывает все четыре типа оповещений из этого руководства — баланс, всплеск ошибок, очередь и суточную сводку. Заводить отдельный вебхук под каждый тип события не нужно: типы различаются цветом embed-сообщения и полем title, а канал остаётся один.
Python: класс для оповещений в Discord
Ниже — компактный класс, который оборачивает вебхук Discord и собирает четыре типа оповещений: баланс, всплеск ошибок, глубину очереди и суточную сводку. PipelineMonitor хранит результаты решений за последние 5 минут и решает, когда пора отправлять сообщение, с учётом задержки cooldown между оповещениями одного типа.
import os
import time
import requests
from datetime import datetime
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
DISCORD_WEBHOOK = os.environ["DISCORD_WEBHOOK_URL"]
session = requests.Session()
class DiscordCaptchaAlerts:
COLORS = {
"success": 0x2ECC71, # Green
"warning": 0xF39C12, # Orange
"error": 0xE74C3C, # Red
"info": 0x3498DB, # Blue
}
def __init__(self, webhook_url):
self.webhook_url = webhook_url
def send_embed(self, title, description, color_key="info", fields=None):
embed = {
"title": title,
"description": description,
"color": self.COLORS.get(color_key, self.COLORS["info"]),
"timestamp": datetime.utcnow().isoformat() + "Z",
"footer": {"text": "CaptchaAI Pipeline Monitor"}
}
if fields:
embed["fields"] = fields
payload = {"embeds": [embed]}
resp = requests.post(
self.webhook_url, json=payload, timeout=10
)
resp.raise_for_status()
def balance_alert(self, balance, threshold):
severity = "error" if balance < 2 else "warning"
self.send_embed(
title="💰 Balance Alert",
description=f"CaptchaAI balance is **${balance:.2f}**",
color_key=severity,
fields=[
{"name": "Threshold", "value": f"${threshold:.2f}", "inline": True},
{"name": "Severity", "value": severity.upper(), "inline": True},
{"name": "Action", "value": "Top up your balance at captchaai.com", "inline": False}
]
)
def error_spike(self, error_rate, error_count, total_count, top_errors):
error_list = "\n".join(
f"• `{code}`: {count}" for code, count in top_errors.items()
)
self.send_embed(
title="⚠️ Error Rate Spike",
description=f"Error rate: **{error_rate:.1%}** ({error_count}/{total_count})",
color_key="error",
fields=[
{"name": "Error Breakdown", "value": error_list or "No details", "inline": False},
{"name": "Window", "value": "Last 5 minutes", "inline": True}
]
)
def queue_alert(self, depth, workers_active):
self.send_embed(
title="📊 Queue Backup",
description=f"Queue depth: **{depth}** pending tasks",
color_key="warning",
fields=[
{"name": "Active Workers", "value": str(workers_active), "inline": True},
{"name": "Est. Drain Time", "value": f"{depth // max(workers_active, 1)} min", "inline": True}
]
)
def daily_summary(self, stats):
self.send_embed(
title="📈 Daily CAPTCHA Summary",
description=f"**{stats['total']}** tasks processed",
color_key="success" if stats["success_rate"] > 0.92 else "warning",
fields=[
{"name": "Success Rate", "value": f"{stats['success_rate']:.1%}", "inline": True},
{"name": "Avg Latency", "value": f"{stats['avg_latency']:.1f}s", "inline": True},
{"name": "Total Cost", "value": f"${stats['cost']:.2f}", "inline": True},
{"name": "Errors", "value": str(stats["errors"]), "inline": True},
{"name": "Balance", "value": f"${stats['balance']:.2f}", "inline": True},
{"name": "Peak Queue", "value": str(stats["peak_queue"]), "inline": True},
]
)
def solve_recovered(self, previous_rate, current_rate):
self.send_embed(
title="✅ Pipeline Recovered",
description=f"Solve rate recovered: {previous_rate:.1%} → {current_rate:.1%}",
color_key="success"
)
alerts = DiscordCaptchaAlerts(DISCORD_WEBHOOK)
class PipelineMonitor:
def __init__(self, check_interval=60):
self.check_interval = check_interval
self.results = [] # (timestamp, success, error_code)
self.last_balance_alert = 0
self.last_error_alert = 0
self.cooldown = 300 # 5 minutes between alerts
def record(self, success, error_code=None):
self.results.append((time.time(), success, error_code))
# Keep last 5 min
cutoff = time.time() - 300
self.results = [r for r in self.results if r[0] > cutoff]
def run_checks(self):
now = time.time()
# Balance check
if now - self.last_balance_alert > self.cooldown:
balance = self._check_balance()
if balance is not None and balance < 10:
alerts.balance_alert(balance, threshold=10)
self.last_balance_alert = now
# Error rate check
if now - self.last_error_alert > self.cooldown and len(self.results) > 10:
total = len(self.results)
errors = [r for r in self.results if not r[1]]
error_rate = len(errors) / total
if error_rate > 0.15:
error_breakdown = {}
for _, _, code in errors:
if code:
error_breakdown[code] = error_breakdown.get(code, 0) + 1
alerts.error_spike(error_rate, len(errors), total, error_breakdown)
self.last_error_alert = now
def _check_balance(self):
try:
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:
return float(data["request"])
except Exception:
pass
return None
monitor = PipelineMonitor()
JavaScript: клиент для вебхука Discord
Тот же набор оповещений на Node.js, если конвейер уже работает на JavaScript — сигнатуры функций совпадают с методами Python-класса выше, так что перенос логики между сервисами занимает минимум времени:
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK_URL;
const COLORS = {
success: 0x2ecc71,
warning: 0xf39c12,
error: 0xe74c3c,
info: 0x3498db,
};
async function sendDiscordEmbed(title, description, colorKey = "info", fields = []) {
await axios.post(DISCORD_WEBHOOK, {
embeds: [
{
title,
description,
color: COLORS[colorKey] || COLORS.info,
timestamp: new Date().toISOString(),
footer: { text: "CaptchaAI Pipeline Monitor" },
fields,
},
],
}, { timeout: 10000 });
}
async function alertBalance(balance, threshold = 10) {
const severity = balance < 2 ? "error" : "warning";
await sendDiscordEmbed(
"💰 Balance Alert",
`CaptchaAI balance is **$${balance.toFixed(2)}**`,
severity,
[
{ name: "Threshold", value: `$${threshold.toFixed(2)}`, inline: true },
{ name: "Severity", value: severity.toUpperCase(), inline: true },
]
);
}
async function alertErrorSpike(errorRate, details = {}) {
await sendDiscordEmbed(
"⚠️ Error Rate Spike",
`Error rate: **${(errorRate * 100).toFixed(1)}%**`,
"error",
[
{ name: "Total Tasks", value: String(details.total || 0), inline: true },
{ name: "Errors", value: String(details.errors || 0), inline: true },
]
);
}
async function sendDailySummary(stats) {
const color = stats.successRate > 0.92 ? "success" : "warning";
await sendDiscordEmbed(
"📈 Daily CAPTCHA Summary",
`**${stats.total}** tasks processed`,
color,
[
{ name: "Success Rate", value: `${(stats.successRate * 100).toFixed(1)}%`, inline: true },
{ name: "Avg Latency", value: `${stats.avgLatency.toFixed(1)}s`, inline: true },
{ name: "Balance", value: `$${stats.balance.toFixed(2)}`, inline: true },
]
);
}
// Balance monitoring loop
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);
if (balance < 10) await alertBalance(balance);
}
} catch (err) {
console.error("Balance check failed:", err.message);
}
}
setInterval(monitorBalance, 300000); // Every 5 minutes
module.exports = { alertBalance, alertErrorSpike, sendDailySummary };
Оба клиента — Python и JavaScript — используют одну и ту же схему цвета embed (зелёный для успеха, оранжевый для предупреждения, красный для ошибки, синий для информационных сообщений), так что оповещения выглядят одинаково независимо от того, каким сервисом конвейера они отправлены. Это важно, если часть пайплайна написана на Python, а часть — на Node.js: дежурный в канале видит единый визуальный язык, а не разрозненный набор форматов.
Пример из практики
Команда автоматизации, распределённая между Алматы и Белградом, гоняет парсер каталога маркетплейса на плане STANDARD ($30/мес, 15 потоков) и следит за задачами из разных часовых поясов. Вместо отдельного дашборда для каждого разработчика все оповещения — баланс, ошибки, суточная сводка — падают в один канал Discord: любой участник видит статус конвейера, не открывая внутренний UI и не дожидаясь, пока коллега в другом городе проснётся и проверит логи вручную.
Типичные ошибки и их решения
Большинство сбоев вебхука сводятся к четырём причинам — все они видны по коду ответа Discord и лечатся без переразвёртывания сервиса:
| Проблема | Причина | Решение |
|---|---|---|
| 400 Bad Request | Некорректная структура embed-сообщения | Проверьте формат массива fields — все значения value должны быть строками |
| Ограничение частоты (429) | Слишком много сообщений в минуту | Добавьте задержку между оповещениями одного типа (минимум 5 минут) |
| Вебхук удалён | Кто-то удалил его на сервере Discord | Создайте новый вебхук и обновите переменную окружения |
| Embed не отображается | Отсутствует массив embeds в payload |
Оберните объект embed в {"embeds": [...]} |
Частые вопросы
Сколько вебхуков Discord можно создать в одном канале?
До 15 на канал. Одного вебхука для CaptchaAI Alerts достаточно — используйте его для всех типов оповещений, различая их цветом embed и полем title.
Как настроить упоминание дежурного при критической ошибке?
Добавьте в payload поле "content": "<@USER_ID>" для конкретного человека или "content": "<@&ROLE_ID>" для роли — Discord пришлёт обычное упоминание поверх embed-сообщения.
Что делать, если Discord возвращает 429 при частых оповещениях?
Это ограничение частоты запросов вебхука. Не отправляйте оповещение чаще, чем раз в 5 минут для одного типа события — в примере выше это уже реализовано через cooldown = 300. Если событий много, агрегируйте их в одну суточную сводку вместо отдельного embed на каждое.
Что будет с таймерами cooldown, если скрипт мониторинга перезапустится?
last_balance_alert и last_error_alert в PipelineMonitor живут только в памяти процесса, поэтому при перезапуске они обнуляются — первая же проверка после старта может отправить оповещение раньше пяти минут с прошлого. Для долгоживущего процесса это не критично, но если скрипт перезапускается часто (например, в serverless-функции), выносите эти метки во внешнее хранилище — Redis или файл — вместо переменных класса.
Можно ли отправлять одни и те же оповещения одновременно в Discord и Slack?
Да — оба канала принимают JSON по HTTPS, поэтому логику PipelineMonitor менять не нужно. Оставьте метод send_embed как есть и добавьте рядом второй клиент с форматом сообщений под Slack; оба вызова можно делать из одного и того же обработчика события, без дублирования бизнес-логики мониторинга.
Дальнейшие шаги
Начните с получения API-ключа CaptchaAI, разверните вебхук за пять минут и подключите первые оповещения к рабочему каналу Discord.