Отсутствующая страница и заблокированный обход требуют разной реакции. В нашем живом прогоне запрос страницы, которой нет на example.com, вернулся как HTTP 200 с cb_status=200 и original_status=404. Обход удался и был платным. Запрос к Glassdoor с Normal token вернулся как HTTP 520 с cb_status=705 и original_status=403. Этот обход не удался и был бесплатным. Монитор, который читает только внешний HTTP-статус, не отличит эти два случая.
Эта статья превращает такие сигналы в дашборд мониторинга веб-скрейпинга. Небольшой клиент записывает каждую завершённую попытку, Prometheus собирает счётчики и гистограммы, а Grafana показывает состояние по доменам рядом с использованием на уровне аккаунта из Account API. Три правила оповещения покрывают устойчивые сбои получения, ответы на Normal token, требующие решения о маршрутизации, и бюджет, который по прогнозу закончится.
В сопутствующем репозитории есть полный стек, девять папок пошаговых примеров, модульные тесты и скрипты живой проверки. Приведённые ниже цифры взяты из живого прогона в ноябре 2026 года, который намеренно смешал простые цели, защищённый сайт на неподходящем токене и URL, возвращающий HTTP 500. Они показывают, что мониторинг делает со сбоями. Это не бенчмарк доли успеха Crawlbase.
- Каждый ответ Crawling API несёт четыре сигнала: внешний HTTP-статус,
cb_status,original_statusи уровень сложности домена. Записывайте все четыре с ключом по имени хоста, никогда по URL. - Платные и успешные ответы: разные множества. Полученная страница 404 платная. Неудачный 520 и таймаут клиента бесплатны. Держите эти два определения раздельно в метриках и на дашборде.
- Локальные счётчики отвечают на вопрос «что происходит сейчас». Account API отвечает на вопрос «сколько аккаунт использовал за этот месяц», раз в пять минут. Сравнивайте их, но не ждите совпадения.
Отделите здоровье обхода от ответа целевого сайта
Ответ Crawling API содержит метаданные, которые отвечают на три разных вопроса: обработал ли Crawlbase запрос, что произошло при получении и что ответил целевой сайт?
| Сигнал | Что описывает | Как его использует дашборд |
|---|---|---|
| Внешний HTTP-статус | Ответ Crawlbase клиенту | Отделяет полученные ответы от транспортных сбоев; неудачные обходы приходят как 520 |
cb_status |
Оценка обхода со стороны Crawlbase | Определяет успех получения и группирует неудачные исходы |
original_status |
Собственный статус ответа целевого сайта | Отделяет отсутствующие страницы, отказы в доступе и ошибки вышестоящего сервера |
domain_complexity |
Ценовой уровень домена | Группирует задержку по уровням |
В записанных HTML-ответах уровень приходил в заголовке x-domain-complexity . С format=json он появлялся как domain_complexity в конверте ответа. Парсер сигналов принимает оба написания, нормализует имена заголовков и читает JSON-конверт, когда запрос его требовал. Он также принимает устаревший псевдоним pc_status , хотя само приложение везде отдаёт cb_status .
cb_status говорит, почему Crawlbase сдался, а original_status говорит, что ответил сайт.В пошаговом примере с сырыми запросами эти сигналы печатаются для нескольких URL. Эти два наблюдения взяты из прогона:
https://example.com/does-not-exist http_status 200 cb_status 200 original_status 404 domain_complexity standard seconds 1.4 https://www.glassdoor.com/Reviews/index.htm http_status 520 cb_status 705 original_status 403 domain_complexity standard seconds 20.3
На диаграмме выше показан другой запрос к Glassdoor из того же прогона, который завершился с cb_status=576; в листинге приведён тот, что вернул 705. Оба кода относятся к одному семейству сбоев. Отсутствующая страница завершилась за 1.4 секунды. Glassdoor вернул неудачный ответ через 20.3 секунды, а второй запрос к тому же URL занял 68 секунд, прежде чем завершиться сбоем. Значит, 404 может быть успешным получением, даже если извлекать из него нечего. Проверка обязательных полей относится к этапу после получения и лежит вне определения успеха этого дашборда.
Прогон также вернул уровень complex_ii , поэтому парсер принимает любой короткий слаг в нижнем регистре вместо фиксированного списка из трёх имён. Это соответствует более широкому диапазону уровней на странице цен. Наблюдавшиеся значения cb_status включали 0, 515, 521, 576, 611 и 702 наряду с задокументированными исходами. Справочник статусов объясняет распространённые коды и их семейства. Держите незнакомые коды видимыми на дашборде; номер кода сам по себе не устанавливает первопричину.
HTTP 520 обозначает неудачный обход, но это не единственный ответ об ошибке. Аутентификация, ограничение частоты и валидация запроса дают другие HTTP-статусы, а транспортный таймаут не даёт HTTP-ответа вовсе.
Записывайте каждую попытку, не создавая ряд на каждый URL
Инструментированный клиент делает один запрос к Crawling API на каждый вызов get() . Он не бросает исключение на HTTP 520; неудачный обход считается нормальным исходом с меткой. Транспортные ошибки учитываются, а затем пробрасываются дальше, так что политика повторов остаётся за вызывающим кодом. Клиент сам никогда не переключает токены и не повторяет URL. Вот путь обработки ответа, как он выглядит в репозитории:
def get(self, url: str, **params: Any) -> CrawlResult: domain = self._limit_domain(domain_label(url)) token_type = "js" if str(params.get("javascript", "")).lower() == "true" else self.token_type success_codes = self.custom_success_codes | parse_success_codes(params.get("custom_success_codes")) self._ensure_series(domain, token_type) started = self._clock() try: response = self._http.get( f"{self.base_url}/", params={"token": self._token, "url": url, **params} ) except httpx.HTTPError as exc: self._record_failure(domain, token_type, exc) raise elapsed = self._clock() - started json_body = None if params.get("format") == "json": try: json_body = response.json() except ValueError: json_body = None signals = parse_signals(response.status_code, response.headers, json_body) billable = is_billable(signals.cb_status, signals.original_status, success_codes) # Counted on completion, next to the outcome, so success <= attempts in any window. self.metrics.attempts.labels(domain=domain, token_type=token_type).inc() self.metrics.requests.labels( domain=domain, cb_status=status_label(signals.cb_status), original_status=status_label(signals.original_status), http_status=status_label(signals.http_status), token_type=token_type, billable=str(billable).lower(), ).inc() self.metrics.latency.labels(domain=domain, complexity=signals.complexity).observe(elapsed) self.metrics.response_bytes.labels(domain=domain).observe(len(response.content)) return CrawlResult(url, domain, token_type, signals, billable, elapsed, response.content)
Определения метрик отделяют завершённые попытки от отдельных исходов. crawlbase_attempts_total увеличивается на каждый полученный ответ и каждую транспортную ошибку. crawlbase_requests_total несёт метки статусов; транспортная ошибка записывается с none во всех трёх статусах и с billable="false", так что таймауты остаются в знаменателе доли успеха:
def _record_failure(self, domain: str, token_type: str, exc: httpx.HTTPError) -> None: reason = "timeout" if isinstance(exc, httpx.TimeoutException) else "transport" self.metrics.attempts.labels(domain=domain, token_type=token_type).inc() self.metrics.client_errors.labels(domain=domain, token_type=token_type, reason=reason).inc() # Count it as a request too, so success-rate denominators include it. self.metrics.requests.labels( domain=domain, cb_status="none", original_status="none", http_status="none", token_type=token_type, billable="false", ).inc()
Метки определяют, сколько рядов придётся хранить Prometheus. Помощник для доменов приводит имя хоста к нижнему регистру, кодирует его по IDNA, убирает ведущий www. и отбрасывает путь и строку запроса. Поддомены остаются различимыми. Демо использует один DomainLimiter для обоих токен-клиентов; он хранит до 200 имён доменов, а всё сверх этого записывает под other.
Метки статусов принимают целые числа от 0 до 999, с none для отсутствующего значения и other для всего, что вне диапазона. Метка сложности принимает слаг в нижнем регистре длиной не более 24 символов. Эти ограничения сдерживают рост меток, хотя комбинации статуса, домена и уровня всё равно перемножаются. Полным URL и идентификаторам запросов место в диагностических логах, а не в метках метрик. Демо также устанавливает фильтр логирования , который маскирует token= в параметрах запроса, настроенные значения токенов и пути /crawler/<token> в каждой строке лога.
Задайте базовые значения счётчиков до начала трафика
У счётчика, который Prometheus впервые видит равным 1, нет наблюдаемого перехода из нуля. rate() не может восстановить это первое событие по более поздним выборкам. Поэтому демо заранее регистрирует известные домены и ждёт 20 секунд перед первым запросом, чтобы 15-секундный сбор увидел нулевые значения:
def preregister(self, urls: Iterable[str], token_type: str | None = None) -> None: """Create zero-valued series before traffic starts. Prometheus only sees a counter once it exists. A series born at 1 between two scrapes shows no increase, so the first failure would be invisible. """ for url in urls: self._ensure_series(self._limit_domain(domain_label(url)), token_type or self.token_type) def _ensure_series(self, domain: str, token_type: str) -> None: if (domain, token_type) in self._known: return self._known.add((domain, token_type)) self.metrics.attempts.labels(domain=domain, token_type=token_type) self.metrics.requests.labels( domain=domain, cb_status="200", original_status="200", http_status="200", token_type=token_type, billable="true", )
preregister() создаёт ряды попыток и ряды успеха по умолчанию 200/200 для каждого домена и типа токена. Все возможные исходы она не создаёт. Комбинация статусов, появившаяся впервые, всё ещё может потерять своё первое событие в разбивке по rate. Отдельная базовая линия попыток делает знаменатель успеха надёжным, не требуя заранее каждой метки сбоя.
Классифицируйте платные ответы отдельно от успешного извлечения
Классификатор биллинга следует задокументированному правилу Crawling API:
CHARGED_CB_STATUSES = frozenset({200, 207}) CHARGED_ORIGINAL_STATUSES = frozenset({200, 201, 301, 302, 303, 307, 308, 404, 410}) def is_billable( cb_status: int | None, original_status: int | None, custom_success_codes: Iterable[int] = (), ) -> bool: if cb_status not in CHARGED_CB_STATUSES or original_status is None: return False return original_status in CHARGED_ORIGINAL_STATUSES or original_status in set(custom_success_codes)
Чтобы ответ был тарифицирован, ему нужны и подходящий cb_status , и подходящий статус целевого сайта. Успешный обход страницы 404 или 410 платный. Если вы передаёте custom_success_codes=403, Crawlbase считает ответ 403 успешным обходом, возвращает cb_status=200 и берёт за него плату. Локальный классификатор повторяет это правило: он принимает пользовательский код только вместе с cb_status 200 или 207, поэтому никогда не повышает ответ, который сам Crawlbase пометил как неудачный.
В пошаговом примере биллинга есть таблица решений и 41 офлайн-тест. Пошаговый пример с конечной точкой метрик отдаёт счётчики через start_http_server(8000). Две записанные строки показывают метки в работе:
crawlbase_requests_total{billable="true",cb_status="200",domain="example.com",http_status="200",original_status="404",token_type="normal"} 9.0 crawlbase_requests_total{billable="false",cb_status="576",domain="glassdoor.com",http_status="520",original_status="403",token_type="normal"} 3.0
Первый ряд фиксирует девять платных получений отсутствующей страницы. Второй фиксирует три бесплатных сбоя Glassdoor. Ни один не говорит, была ли извлечённая запись корректной.
Дашборд определяет успех получения как cb_status=200. В платную категорию попадают и подходящие ответы 207. Ответ 207 пустой, поэтому он может быть платным и при этом не входить в политику успеха. Держите эти определения раздельно, когда читаете долю успеха и долю платных рядом.
Сравните локальную активность с использованием на уровне аккаунта
Локальные счётчики описывают один инструментированный процесс. Account API возвращает месячную статистику уровня аккаунта по одному продукту, включая успехи, сбои, сумму к оплате и сводки по доменам. Он допускает один запрос в пять минут, что делает его источником другого рода, чем конечная точка /metrics , опрашиваемая каждые 15 секунд.
Поллер аккаунта хранит последний снимок и время последнего вызова на диске. Он записывает метку времени до отправки запроса, поэтому неудачный вызов или прерванный процесс всё равно расходует 300-секундный интервал:
def poll(self, metrics: AccountMetrics | None = None) -> AccountSnapshot | None: """Return a fresh snapshot if allowed, otherwise the cached one.""" if self.seconds_until_allowed() > 0: if metrics: metrics.cache_hits.inc() return self._cached self._last_call = self._clock() self._save_state() try: response = self._http.get( f"{self.base_url}/account", params={"token": self._token, "product": self.product} ) response.raise_for_status() snapshot = parse_account(response.json()) except (httpx.HTTPError, ValueError) as exc: result = "timeout" if isinstance(exc, httpx.TimeoutException) else "error" status = exc.response.status_code if isinstance(exc, httpx.HTTPStatusError) else None log.warning("Account API call failed (%s): %s status=%s", result, type(exc).__name__, status) if metrics: metrics.api_calls.labels(result=result).inc() return self._cached self._cached = snapshot self._save_state() if metrics: metrics.api_calls.labels(result="ok").inc() return snapshot
При запуске поллер перечитывает сохранённое время вызова и снимок. Пошаговый пример реконсилера показывает второй вызов, немедленно отвеченный из кэша, а в записанном тесте новый процесс тоже переиспользовал сохранённый ответ.
Запускайте один реконсилер на токен. Файл состояния поддерживает последовательные перезапуски, а не координацию между одновременными поллерами. Каждый экземпляр хранит собственную метку времени в памяти и не берёт межпроцессную блокировку. При тестировании два поллера с отдельными файлами состояния получили отказ за слишком ранний вызов, и один общий файл сам по себе не делает одновременные поллеры безопасными.
Задокументированная таблица полей описывает итоги и разбивки по доменам. В записанной нагрузке они были вложены в currentMonth и metrics:
{ "currentMonth": { "totalSuccess": "<str>", "totalFailed": "<str>", "totalDue": "<float>", "domainStats": [ { "domain": "<str>", "metrics": { "totalRequests": "<int>", "success": "<int>", "failure": "<int>", "successRate": "<str>" } } ] } }
Парсер принимает эту наблюдаемую структуру и плоскую. Он преобразует строковые итоги и процентные строки вроде "100.0%", а где есть счётчики по доменам, вычисляет долю из успехов и общего числа запросов. Живые имена доменов были голыми метками вроде glassdoor , а не glassdoor.com. Данные отставали от трафика на несколько минут, а product=crawler вернул ту же месячную структуру.
Prometheus может собирать gauge-метрики реконсилера каждые 60 секунд, не вызывая Account API на каждом сборе. Вызовы, попавшие внутрь защитного интервала, возвращают кэшированные данные, а долгоживущий реконсилер просто ждёт, пока следующий вызов не будет разрешён.
Правила записи выводят метку site , чтобы имена хостов можно было сравнивать с голыми метками аккаунта. Это простое регулярное выражение по имени хоста, а не разбор с учётом публичных суффиксов. Оно работает для целей .com и .org в демо; адаптируйте сопоставление, прежде чем направлять его на example.co.uk.
Разберитесь в запасном варианте для бюджета
remainingCredits отсутствовал в записанном ответе Crawling API. В документации отмечено, что он возвращается только для продуктов по подписке, поэтому реконсилеру нужен запасной вариант. Если задан CRAWLBASE_MONTHLY_BUDGET , снимок экспортируется как max(0, budget - totalSuccess) под source="budget". Когда API всё же передаёт остаток кредитов, gauge-метрика использует source="account_api" :
metrics.total_success.set(snapshot.total_success) metrics.total_failed.set(snapshot.total_failed) metrics.total_due.set(snapshot.total_due) if snapshot.remaining_credits is not None: metrics.remaining_credits.labels(source="account_api").set(snapshot.remaining_credits) elif monthly_budget: metrics.remaining_credits.labels(source="budget").set( max(0.0, monthly_budget - snapshot.total_success) )
Запасной вариант: оценка по числу запросов. Правило «дней до нуля» делит gauge-метрику остатка на частоту платных запросов за последний час, умноженную на 86 400, что предполагает один кредит на платный запрос. Рендеринг JavaScript и более высокие уровни сложности расходуют больше кредитов, а другие продукты могут тратить тот же баланс. Прогноза достаточно, чтобы продемонстрировать оповещение о бюджете. Прежде чем считать его финансовым лимитом, используйте расход, взвешенный по кредитам.
Соедините экспортеры, Prometheus и Grafana
Получение и сверка работают как отдельные процессы. Каждый отдаёт конечную точку метрик, Prometheus собирает оба и вычисляет правила записи, а Grafana запрашивает эти правила для панелей и оповещений.
В пошаговом примере с Compose появляются четыре сервиса: скрейпер на порту 8000, реконсилер на 8001, Prometheus на 9090 и Grafana на 3000. Финальный стек закрепляет Prometheus 3.1.0 и Grafana 11.4.0, а его зависимости Python закрепляют HTTPX 0.28.1, prometheus-client 0.21.1 и python-dotenv 1.0.1. Конфигурация сбора короткая:
global: scrape_interval: 15s evaluation_interval: 15s rule_files: - /etc/prometheus/rules/*.yml scrape_configs: - job_name: crawlbase-scraper static_configs: - targets: ["scraper:8000"] # The gauges only change every 5 minutes, a slower scrape is plenty. - job_name: crawlbase-account scrape_interval: 60s static_configs: - targets: ["reconciler:8001"]
Правила вычисляются глобально каждые 15 секунд, а для группы записи Crawlbase задано 30 секунд. Grafana обновляет дашборд каждые 30 секунд и вычисляет группу оповещений каждую минуту. Ни одно из этих расписаний не является пятиминутным ограничением Account API; это отдельные часы.
Именованный том cbwatch-account-state общий для пошаговых стеков и финального стека. Обычная команда docker compose down его сохраняет, так что можно переходить между стеками, не теряя защитный интервал. Удаление тома удаляет и сохранённое время вызова.
В пошаговом примере дашборда вводятся правила записи, чтобы панели и оповещения разделяли одно определение. Центральная доля по доменам:
# Success = the crawl worked (cb_status 200). The denominator is # crawlbase_attempts_total, which also counts timeouts and is created at 0. - record: crawlbase:domain_success_ratio:rate5m expr: | clamp_max( sum by (domain) (rate(crawlbase_requests_total{cb_status="200"}[5m])) / sum by (domain) (rate(crawlbase_attempts_total[5m])), 1 )
Числитель считает cb_status=200. Знаменатель: каждая завершённая попытка, включая таймауты, с объединением обоих типов токенов по домену. clamp_max ограничивает долю значением 1, когда два раздельно выбранных счётчика дают кратковременное превышение. Часовая сводка использует то же определение на более длинном окне.
Тесты правил Prometheus прогоняют через правила синтетические последовательности счётчиков. Их последовательность для Walmart даёт 90% успеха, а вторая последовательность подтверждает, что таймауты снижают долю. Это фикстуры, а не живые наблюдения.
Читайте каждую панель дашборда по её собственному определению
Grafana загружает дашборд из провизионированного JSON. Генератор дашборда строит этот JSON из Python, а модульный тест проверяет, что закоммиченный файл совпадает с генератором. Панель доли успеха указывает на общее правило:
timeseries("Success rate per domain", [target("crawlbase:domain_success_ratio:rate5m", "{{domain}}")], 0, 6, unit="percentunit", minv=0, maxv=1, desc="cb_status 200 share per domain, 5 minute window. Alert fires under 90% for 10m."),
Схема раскладывает записанные наблюдения по операционным вопросам. Читайте каждое значение вместе с окном его запроса; часовой rate, пятиминутный rate и сырой итог счётчика отвечают на разные вопросы, и их нельзя сводить в одну долю успеха пакета.
| Панель или диагностика | Записанное наблюдение | Интерпретация |
|---|---|---|
| Доля успеха, один час | 65.1% | Успех получения в демо, которое намеренно содержит сбои |
| Доля платных, один час | 65.1% | Оконная доля платных, отдельно от сырых итогов счётчиков |
| Остаток бюджета и прогноз | 890 из бюджета 1 000; 0.7 дня | Оценка «один кредит на запрос» |
| Успех по доменам | example.com 100%; glassdoor.com 15%; httpbin.org 0% | Здоровье получения по доменам; цель httpbin намеренно возвращает 500 |
| Задержка p95, уровень standard | 87.8 секунды | Оценка по гистограмме только по полученным ответам |
| Домены с наибольшим числом сбоев | glassdoor.com и httpbin.org | 20 исходов не-200, включая таймауты |
| Доля блокировок Glassdoor на Normal token | 41% | Отдельная диагностика доли блокировок; панель дашборда рисует запросы в минуту |
| Счётчики платных и бесплатных | 46 платных; 20 бесплатных | Те же 66 попыток, что и в разбивке по статусам |
| Локальный успех против успеха аккаунта | 65.1% против 70.1% | Локальная часовая доля против доли аккаунта с начала месяца |
| Glassdoor: локально против аккаунта | 15% против 36.4% | Та же разница окна и охвата на уровне сайта |
Разбивка по статусам содержала 46 ответов с 200, шесть таймаутов клиента, четыре с 576, четыре с 0, два с 704, два с 705 и по одному с 521 и 611. Все 46 ответов с 200 были платными, остальные 20 бесплатными, так что метка платности и разбивка по статусам описывают те же 66 попыток. Все 20 бесплатных исходов пришлись на glassdoor.com и httpbin.org. Каждый cb_status не-200 пришёл как HTTP 520; у таймаутов ответа не было, и они записаны как none. Оконные rate и сырые счётчики всё равно различаются: часовая доля успеха не равна сырому отношению 46 из 66.
Для p95 в 87.8 секунды нужен точный знаменатель. Клиент измеряет задержку только после прихода ответа, включая HTTP 520. Таймауты клиента в гистограмму не попадают. Значение является интерполяцией по бакетам, а не точным перцентилем по отдельным длительностям, и граница верхнего бакета равна 90-секундному таймауту. Обычные страницы наблюдались около двух секунд. 90-секундный таймаут HTTPX применяется к каждой сетевой операции, включая ожидание следующего фрагмента, поэтому это не строгий дедлайн по настенным часам, как объясняет документация HTTPX по таймаутам .
У разрыва между локальными цифрами и цифрами аккаунта несколько ожидаемых причин. Локальные метрики покрывают попытки этого процесса и включают ошибки на стороне клиента. Account API покрывает весь месяц для каждого вызывающего, использующего выбранный продукт, и может отставать от недавнего трафика. Такие охваты не дают ни сверки один к одному со счётом, ни одинакового учёта таймаутов.
Панель возраста данных значит меньше, чем подсказывает её название. Реконсилер обновляет метку времени всякий раз, когда применяет снимок, включая кэшированные данные после неудачного опроса или перезапуска. Читайте её рядом со счётчиками ошибок API; сама по себе она не доказывает, что данные были свежо получены.
Сделайте оповещения о доле успеха скрейпинга действенными
В пошаговом примере оповещений провизионируются три правила, управляемые Grafana. Они отделяют устойчивую потерю получения от расследования маршрутизации токенов и предупреждения о бюджете:
| Оповещение | Условие | Предполагаемая обработка |
|---|---|---|
| Успех домена ниже 90% | Пятиминутная доля успеха ниже 0.9 в течение 10 минут на доменах с частотой попыток выше 0.008/s | Вызов |
| Normal token заблокирован | Выбранные исходы на Normal token выше 20% попыток в течение пяти минут | Тикет |
| Кредиты по прогнозу закончатся | Оценка дней до нуля меньше трёх в течение 15 минут | Вызов |
Порог частоты в оповещении по домену составляет примерно один запрос каждые 125 секунд, или 2.4 запроса на пятиминутное окно. Он отфильтровывает почти простаивающие домены, но не гарантирует большой выборки. Задайте нижнюю границу трафика и порог под объём и надёжность, которые действительно нужны вашей задаче.
- uid: cb-domain-success-low title: Crawlbase domain success rate below 90% condition: C for: 10m noDataState: OK execErrState: Error labels: severity: page annotations: summary: "Success rate for {{ $labels.domain }} is {{ humanizePercentage $values.B.Value }}" description: >- cb_status 200 share has been under 90% for 10 minutes. Open the cb_status breakdown panel: 5xx/6xx codes are the crawl layer, 704/705/706 mean the target site said no (check original_status). runbook_url: https://crawlbase.com/docs/status-codes/ data: - refId: A relativeTimeRange: { from: 600, to: 0 } datasourceUid: prometheus model: refId: A instant: true # Ignore domains with almost no traffic (under ~1 request per 2 minutes). expr: crawlbase:domain_success_ratio:rate5m and on (domain) crawlbase:domain_requests:rate5m > 0.008 - refId: B datasourceUid: __expr__ model: { refId: B, type: reduce, expression: A, reducer: last } - refId: C datasourceUid: __expr__ model: refId: C type: threshold expression: B conditions: - evaluator: { type: lt, params: [0.9] }
Правило блокировки сопоставляет 503|207|525|576|705 на Normal token. Документация предлагает JavaScript token для 207 и 503; проект сохраняет 525 для старых ответов и добавляет наблюдавшиеся исходы 576 и 705. Эти добавления являются политикой приложения, а не правилом продукта. Устойчивый 403 всё равно требует взглянуть на ответ целевого сайта и вашу частоту запросов, а не предполагать, что смена токена исправит любой случай.
В живом прогоне первым сработало оповещение о Normal token, для glassdoor.com. Оповещение об успехе домена сработало три минуты спустя, а оповещение о бюджете перешло в ожидание, как только его оценка частоты устоялась.
Репозиторий провизионирует правила и метки серьёзности, а не Slack, электронную почту, PagerDuty или интеграцию с тикетами. Настройте точки контакта и политики уведомлений Grafana , чтобы направлять severity=page и severity=ticket туда, куда нужно. Сама по себе метка никого не вызовет.
Одиночный 520, один медленный ответ или известный 404 относятся к дашборду, а не к вызову дежурного. Устойчивое изменение доли сбоев, времени завершения или покрытия обязательных полей заслуживает человека. Провизионированные правила считают отсутствие данных нормой, поэтому доступность экспортеров и остановленные задания отслеживайте отдельно.
Аннотации Grafana используют одиночный $ в ссылках вроде $labels.domain и $values.B.Value. Записанное поведение Grafana 11 оставляло удвоенные знаки доллара буквально, поэтому репозиторий тестирует написание шаблона вместе с порогами.
Распространите те же метрики на очереди Crawler и маршрутизацию токенов
Для Crawler необязательный поллер статистики читает /crawler/<TOKEN>/stats и экспортирует итоги аккаунта плюс gauge-метрики ожидания, параллельности, задержки и паузы по каждому краулеру. С CRAWLER_STATS_ENABLED=true реконсилер пытается выполнять эти опросы по номинальному 60-секундному расписанию, которое синхронные вызовы API могут задерживать.
В пошаговом примере с Crawler и несколькими токенами тоже по одному клиенту на токен, а Glassdoor направляется через настроенный набор JS_DOMAINS . В записанном прогоне запрос с JavaScript token вернул чистый 200. Оператор обновляет этот набор после расследования оповещения; само оповещение никогда не переписывает маршрутизацию:
# Filled from the alert: Normal token gets cb_status 503/576/705 on these. JS_DOMAINS = {"glassdoor.com"} URLS = [ "https://example.com/", "https://www.glassdoor.com/Reviews/index.htm", ] def main() -> None: load_dotenv(find_dotenv(usecwd=True)) clients = { "normal": InstrumentedCrawlingAPI(os.environ["CRAWLBASE_NORMAL_TOKEN"], "normal"), "js": InstrumentedCrawlingAPI(os.environ["CRAWLBASE_JS_TOKEN"], "js"), } for url in URLS: token_type = "js" if domain_label(url) in JS_DOMAINS else "normal" r = clients[token_type].get(url) print(f"{token_type:<6} http={r.status_code} cb_status={r.headers.get('cb_status')} {url}")
Оба клиента пишут в те же семейства метрик с разными значениями token_type , так что можно изучать сбои конкретного токена, сохраняя один вид по доменам. Имена краулеров должны так же обозначать стабильные очереди, никогда отдельные задания или URL.
Запустите стек и проверьте интеграцию
Из корня репозитория запустите финальный стек:
cd final cp .env.example .env # add CRAWLBASE_NORMAL_TOKEN and CRAWLBASE_JS_TOKEN docker compose up -d --build # Grafana: http://localhost:3000/d/crawlbase-observability (admin / admin)
Файл финального шаблона окружения включает бюджет и необязательные настройки Crawler. Корневой шаблон задаёт токены для пошаговых папок, а сгенерированные файлы .env игнорируются git. Финальный стек по умолчанию отправляет 40 запросов, а затем продолжает отдавать метрики. Успешные получения расходуют кредиты. Задайте пароль Grafana, прежде чем открывать стек за пределы своей машины.
Файл README описывает каждый этап и проверку. В опубликованной раскладке final/ и steps/ лежат прямо в корне; команды README, сохраняющие префикс code/ , относятся к прежней раскладке с родительской папкой.
Проект поставляется с 319 офлайн-тестами на pytest и respx плюс шестью живыми тестами, выбираемыми отдельно по маркеру. Офлайн-проверка прошла все 319 и 41 тест биллинга из пошагового примера. Тесты с замоканным HTTP покрывают парсеры, классификацию статусов, обновление метрик, поведение кэша и обработку перезапуска.
Скрипт проверки конфигурации запускает проверки конфига и правил Prometheus, синтетические тесты правил и валидацию дашборда. Smoke-тест проверяет у работающего стека цели, экспортированные ряды, правила записи, дашборд, источник данных и вычисление правил оповещения. Живой зонд, захват образцов и скрипт запросов панелей раскрывают формы ответов и запросы, стоящие за цифрами в этой статье.
Заключение
Полезная наблюдаемость скрейпинга начинается с разделения трёх вещей: результата обхода, ответа целевого сайта и вашей собственной политики приёмки. Crawlbase даёт сигналы получения, обёртка их записывает, Prometheus превращает их в оконные метрики, а Grafana делает устойчивые изменения видимыми.
Живой прогон показал, почему эти различия важны. Корректные получения 404 были платными, защищённая цель дала несколько кодов сбоя, таймаутам клиента понадобился собственный учёт, а статистика на уровне аккаунта отличалась от локального часового вида. Проект делает всё это проверяемым, не размечая каждую страницу и не встраивая платформу мониторинга в скрейпер.
Используйте полную реализацию с небольшим набором репрезентативных целей, подтвердите своё определение успеха и допущения о кредитах и подключите оповещения к реальному дежурству. Затем измеряйте те цели, от которых действительно зависит ваше приложение.
Каждый ответ несёт cb_status, original_status и уровень домена, так что ваш мониторинг отличит заблокированный обход от отсутствующей страницы. Неудачные запросы никогда не тарифицируются, а Account API даёт месячные итоги по доменам. Начните с до 5 000 бесплатных запросов, без карты.
Часто задаваемые вопросы (FAQ)
Что считать успешным запросом скрейпинга?
Этот проект считает cb_status=200 успехом получения и помещает каждую завершённую попытку в знаменатель. Целевой 404 этому определению соответствует; пустой 207 не соответствует. Для конвейера товаров или статей добавьте отдельные метрики для записей, прошедших ваши проверки обязательных полей и содержимого. Успешное получение и пригодные бизнес-данные: разные вопросы.
Почему доля успеха Account API отличается от локальной доли Grafana?
Локальная сводка: скользящее часовое окно по попыткам этого процесса. Account API сообщает активность с начала месяца по выбранному продукту, может включать других вызывающих и в записанном прогоне отставал от трафика на несколько минут. Выровняйте охваты, прежде чем читать разрыв как пропавший трафик или неверный биллинг.
Показывает ли дашборд точное число оставшихся кредитов?
Когда Account API возвращает remainingCredits, что происходит только для продуктов по подписке, реконсилер экспортирует этот баланс. Иначе он вычитает успешные запросы месяца из настроенного бюджета. Этот запасной вариант и правило «дней до нуля» предполагают один кредит на запрос. Учтите рендеринг, множители доменов и общее использование продуктов, прежде чем полагаться на оценку как на реальный баланс.
Повторяет ли оповещение о заблокированном токене запрос с JavaScript token автоматически?
Нет. Клиент записывает результат и оставляет повторы вызывающему коду. Пример с несколькими токенами направляет домены через настроенный набор, а оповещение подсказывает оператору, куда смотреть. Обновление этой маршрутизации и подключение адресатов уведомлений: явные решения приложения и эксплуатации.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда занималась конвейерами данных, а не поддержкой инфраструктуры обхода. До 5 000 запросов бесплатно, карта не нужна.

