From ac5b044f7e5ae560658d2c72ea615a1c66ca251d Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 14:15:59 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(scrapers):=20=D0=BE=D0=B6=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=D1=8B=D0=B5=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D1=8B=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20(=D0=B1=D0=B0=D0=BD,?= =?UTF-8?q?=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B9=20=D0=BF=D1=83=D0=BB,?= =?UTF-8?q?=20=D0=BA=D0=B0=D0=BF=D1=87=D0=B0)=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20=D0=BD=D0=B5=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скрапер один давал 3006 error-строк в сутки из ~3700 по всему Trade-In — ленту перестали читать, и настоящая поломка терялась в ней. Принцип: ожидаемый исход сбора (площадка забанила, пул прокси пуст, капча/недогруз, серия блоков перевалила порог circuit breaker) — это состояние работы против недружелюбного источника, а не инцидент. В error остаётся только неожиданное: изменившаяся вёрстка/схема (Cian markup change), просроченный токен ротации прокси (ASocks 401), неразобранное исключение. Переведено error -> warning в 9 файлах, 12 мест: "СТОП — пул прокси пуст" (avito/domclick/cian_history/yandex_newbuilding_sweep x2), "пул прокси исчерпан" (cian_session, cian_price_history, yandex_address_backfill), FAIL-CLOSED без здорового узла для source (proxy_egress), ABORT по счётчику подтверждённых блоков площадки (avito, domclick, yandex_detail_backfill). Оставлено error намеренно: cookie-алерты Cian/DomClick (#2658, #2674) — они рассчитаны именно на LoggingIntegration(event_level=ERROR) в scheduler_main.py и без него молчат по 37 дней; ABORT по смешанным/soft причинам без единого подтверждённого блока площадки (#3272, #2674/#3196) — это может быть наш баг, а не бан, сигнал сознательно не приглушали. GlitchTip: сентри-интеграция скрапера уже настроена как LoggingIntegration(level=INFO, event_level=ERROR) в scheduler_main.py — отдельной правки sentry_scrub.py не требуется, понижение уровня logger само убирает эти записи из GlitchTip. Итоговая FINISHED-строка со счётчиками (attempted/enriched/blocked/failed) уже существует в каждом detail_backfill — новую не добавлял. Refs #3471 --- tradein-mvp/backend/app/services/cian_price_history.py | 2 +- tradein-mvp/backend/app/services/cian_session.py | 2 +- tradein-mvp/backend/app/services/proxy_egress.py | 2 +- tradein-mvp/backend/app/services/yandex_address_backfill.py | 2 +- tradein-mvp/backend/app/tasks/avito_detail_backfill.py | 4 ++-- tradein-mvp/backend/app/tasks/cian_history_backfill.py | 2 +- tradein-mvp/backend/app/tasks/domclick_detail_backfill.py | 4 ++-- tradein-mvp/backend/app/tasks/yandex_detail_backfill.py | 2 +- tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tradein-mvp/backend/app/services/cian_price_history.py b/tradein-mvp/backend/app/services/cian_price_history.py index 21146957..b0ba6c4c 100644 --- a/tradein-mvp/backend/app/services/cian_price_history.py +++ b/tradein-mvp/backend/app/services/cian_price_history.py @@ -131,7 +131,7 @@ async def backfill_cian_price_history( # Fail-closed (#2616): пул пуст/недоступен в проде. Остальные листинги # упрутся в то же самое — рвём батч сразу, а не 50 раз по 5 секунд с # логом, который читается как «Циан нас блокирует». - logger.error( + logger.warning( "cian_price_history: нет доступного прокси в пуле (%s) — батч прерван " "на listing_id=%s (обработано %d из %d)", exc, diff --git a/tradein-mvp/backend/app/services/cian_session.py b/tradein-mvp/backend/app/services/cian_session.py index 388d94fb..25d240d1 100644 --- a/tradein-mvp/backend/app/services/cian_session.py +++ b/tradein-mvp/backend/app/services/cian_session.py @@ -201,7 +201,7 @@ async def verify_session(cookies: dict[str, str]) -> dict[str, Any] | None: # ИМЕННО для cian/нездоровы — НЕ уходим на settings.cian_proxy_url (тот самый # статичный узел мог быть источником бана, см. proxy_egress module docstring). # Явный отказ вместо слепого прохода через заведомо подозрительный egress. - logger.error( + logger.warning( "Cian cookies verify: пул прокси исчерпан для cian (%s) — verify пропущен, " "cookies НЕ помечены протухшими, retry на следующем такте", exc, diff --git a/tradein-mvp/backend/app/services/proxy_egress.py b/tradein-mvp/backend/app/services/proxy_egress.py index 8227a40a..12880ea7 100644 --- a/tradein-mvp/backend/app/services/proxy_egress.py +++ b/tradein-mvp/backend/app/services/proxy_egress.py @@ -302,7 +302,7 @@ def resolve_proxy_url(db: Session, source: str) -> str | None: # Сценарий 2: пул РЕАЛЬНО не пуст, но для source не осталось ни одного # здорового/небаненного узла -- fail-closed (#2616), НЕ fallback на env. - logger.error( + logger.warning( "proxy_egress: source=%s -- пул scrape_proxies НЕ пуст (%d узлов), но НИ ОДИН " "не прошёл фильтр для этого источника (banned_for_source=%d, " "unhealthy_or_disabled=%d) -- FAIL-CLOSED (#2616): отказ, БЕЗ обхода через " diff --git a/tradein-mvp/backend/app/services/yandex_address_backfill.py b/tradein-mvp/backend/app/services/yandex_address_backfill.py index 059d3dc2..b5831676 100644 --- a/tradein-mvp/backend/app/services/yandex_address_backfill.py +++ b/tradein-mvp/backend/app/services/yandex_address_backfill.py @@ -141,7 +141,7 @@ async def backfill_yandex_addresses( # нездоровы — НЕ уходим на settings.scraper_proxy_url (см. proxy_egress module # docstring). Явный пропуск run'а вместо слепого прохода через egress, который # мог быть источником текущего инцидента. - logger.error( + logger.warning( "yandex_address_backfill: пул прокси исчерпан для yandex (%s) — run " "пропущен, ни один листинг не обработан", exc, diff --git a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py index 72f4ab24..dcbe755a 100644 --- a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py @@ -806,7 +806,7 @@ async def run_avito_detail_backfill( # и mark_failed с текстом про пул, как у домклика после #3283. if _caused_by_empty_pool(e): counters.failed += 1 - logger.error( + logger.warning( "avito_detail_backfill: run_id=%d СТОП — пул прокси пуст, " "к площадке не ходили. enriched=%d attempted=%d", run_id, @@ -913,7 +913,7 @@ async def run_avito_detail_backfill( # ratio: прогон 5210 (14 блоков из 20, ровно порог) отпечатал # "ABORT -- 1 consecutive blocks" -- текущая серия в тот момент # действительно равнялась единице, но обрыв был не по ней. - logger.error( + logger.warning( "avito_detail_backfill: run_id=%d ABORT -- %s, " "частая причина: %s. enriched=%d attempted=%d", run_id, diff --git a/tradein-mvp/backend/app/tasks/cian_history_backfill.py b/tradein-mvp/backend/app/tasks/cian_history_backfill.py index 9b1299ec..3de9d816 100644 --- a/tradein-mvp/backend/app/tasks/cian_history_backfill.py +++ b/tradein-mvp/backend/app/tasks/cian_history_backfill.py @@ -256,7 +256,7 @@ async def backfill_cian_history( if caused_by_no_proxy(exc): result.no_proxy_stop = True result.listings_failed_fetch += 1 - logger.error( + logger.warning( "cian_history_backfill: СТОП — пул прокси пуст, к площадке " "не ходили. listing_id=%s processed=%d succeeded=%d", listing_id, diff --git a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py index 5f4c6963..d2f96a5c 100644 --- a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py @@ -493,7 +493,7 @@ async def run_domclick_detail_backfill( # ниже. Причина не теряется: в записи прогона стоит # no_proxy_stop=1 и mark_failed с текстом про пул. counters.failed += 1 - logger.error( + logger.warning( "domclick_detail_backfill: run_id=%d СТОП — пул прокси пуст, " "к площадке не ходили. enriched=%d attempted=%d", run_id, @@ -572,7 +572,7 @@ async def run_domclick_detail_backfill( if consecutive_blocks >= max_consecutive_blocks: # #3196: причину больше не выдумываем и не молчим — печатаем # перепись диагнозов по HTTP-статусам этого прогона. - logger.error( + logger.warning( "domclick_detail_backfill: run_id=%d ABORT -- %d consecutive " "blocks, диагнозы: %s. enriched=%d attempted=%d", run_id, diff --git a/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py b/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py index 31e7cad7..6377ef5e 100644 --- a/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py @@ -427,7 +427,7 @@ async def run_yandex_detail_backfill( ) if consecutive_blocks >= max_consecutive_blocks: aborted_by_blocks = True - logger.error( + logger.warning( "yandex_detail_backfill: run_id=%d ABORT -- %d consecutive " "non-200 responses. enriched=%d attempted=%d", run_id, diff --git a/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py b/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py index 0733798d..c4b11b89 100644 --- a/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py +++ b/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py @@ -354,7 +354,7 @@ async def enrich_yandex_newbuilding_sweep( if caused_by_no_proxy(exc): result.no_proxy_stop = True result.failed_resolve += 1 - logger.error( + logger.warning( "yandex-nb-sweep: СТОП — пул прокси пуст, к площадке не ходили. " "house_id=%s processed=%d succeeded=%d", house_id, @@ -419,7 +419,7 @@ async def enrich_yandex_newbuilding_sweep( if caused_by_no_proxy(exc): # #3197 — см. блок resolve выше result.no_proxy_stop = True result.failed_fetch += 1 - logger.error( + logger.warning( "yandex-nb-sweep: СТОП — пул прокси пуст, к площадке не ходили. " "house_id=%s processed=%d succeeded=%d", house_id, From 6608fd5c7005abb75d34f6a848bc04a5aeb47c80 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 14:39:58 +0300 Subject: [PATCH 2/2] =?UTF-8?q?test(scrapers):=20=D0=BF=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20caplog-=D1=84=D0=B8=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D1=80=D1=8B=20=D0=BF=D0=BE=D0=B4=20error->warning=20=D1=88?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D0=BD=D1=8B=D1=85=20=D0=B8=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ветка fix/3471-scraper-log-levels понизила error на warning для штатных исходов скрапинга (пустой/исчерпанный пул прокси, серия подтверждённых блоков площадки) -- 7 тестов фильтровали caplog по ERROR и падали на пустом списке. Поправлен только уровень фильтра/set_level, содержательные assert'ы (streak vs ratio, отсутствие qrator/ip_rate_limited литералов, различимость текстов "исчерпан" и "пуст") не менялись. В test_exhausted_and_empty_pool_log_texts_are_distinct оба сценария (пустой пул и fail-closed) теперь на одном уровне (warning) -- тест адаптирован проверять различимость по тексту, а не по уровню. Refs #3471 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG --- .../backend/tests/services/test_proxy_egress.py | 16 ++++++++++------ .../tests/tasks/test_avito_detail_backfill.py | 12 +++++++++--- .../tests/tasks/test_domclick_detail_backfill.py | 4 +++- tradein-mvp/backend/tests/test_cian_session.py | 9 +++++---- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/tradein-mvp/backend/tests/services/test_proxy_egress.py b/tradein-mvp/backend/tests/services/test_proxy_egress.py index b123bb9c..a82d28e2 100644 --- a/tradein-mvp/backend/tests/services/test_proxy_egress.py +++ b/tradein-mvp/backend/tests/services/test_proxy_egress.py @@ -420,8 +420,10 @@ def test_all_candidates_banned_raises_pool_exhausted_not_env_fallback( assert exc_info.value.pool_total == 2 assert exc_info.value.banned_for_source == 2 assert exc_info.value.unhealthy_or_disabled == 0 - errors = [rec for rec in caplog.records if rec.levelname == "ERROR"] - assert any("fail-closed" in rec.message.lower() for rec in errors) + # fail-closed без здорового узла — штатный исход скрапинга, а не инцидент; + # понижено до warning, чтобы не шуметь в GlitchTip (было logger.error). + warnings = [rec for rec in caplog.records if rec.levelname == "WARNING"] + assert any("fail-closed" in rec.message.lower() for rec in warnings) # НЕ должно быть "обход пула" / "static-fallback" в логах — env не тронут. assert not any("static-fallback" in rec.message for rec in caplog.records) @@ -429,8 +431,10 @@ def test_all_candidates_banned_raises_pool_exhausted_not_env_fallback( def test_exhausted_and_empty_pool_log_texts_are_distinct( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: - """Регрессия на замечание ревью: "пуст" и "все отсеяны" — РАЗНЫЕ формулировки И - разные уровни (WARNING vs ERROR), иначе их нельзя различить в логах/алертах.""" + """Регрессия на замечание ревью: "пуст" и "все отсеяны" — РАЗНЫЕ формулировки, + иначе их нельзя различить в логах/алертах. Оба сценария — штатный исход + скрапинга, поэтому оба теперь warning (было WARNING vs ERROR), различимость + держится на тексте, не на уровне.""" now = datetime.now(UTC) with caplog.at_level("WARNING"): @@ -452,8 +456,8 @@ def test_exhausted_and_empty_pool_log_texts_are_distinct( exhausted_messages = {rec.levelname: rec.message for rec in caplog.records} assert "ERROR" not in empty_pool_messages - assert "ERROR" in exhausted_messages - assert empty_pool_messages.get("WARNING") != exhausted_messages.get("ERROR") + assert "ERROR" not in exhausted_messages + assert empty_pool_messages.get("WARNING") != exhausted_messages.get("WARNING") def test_disabled_proxy_raises_pool_exhausted() -> None: diff --git a/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py b/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py index 3a40aed6..9e963679 100644 --- a/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py +++ b/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py @@ -348,7 +348,9 @@ async def test_backfill_abort_log_has_no_ip_rate_limited_literal(caplog: Any) -> patch(_RESOLVE_PROXY_URL, MagicMock(return_value="http://test-proxy.local:8080")), patch(_FETCH, mock_fetch), patch(_SLEEP, new_callable=AsyncMock), - caplog.at_level("ERROR"), + # серия подтверждённых блоков площадки -- штатный исход скрапинга, не + # инцидент; понижено до warning, чтобы не шуметь в GlitchTip. + caplog.at_level("WARNING"), ): await run_avito_detail_backfill( db, run_id=3, params={"batch_size": 10, "budget_sec": 3600, "max_consecutive_blocks": 5} @@ -1354,7 +1356,9 @@ async def test_backfill_ratio_abort_log_names_the_ratio_not_the_streak(caplog: A patch(_FETCH, mock_fetch), patch(_SAVE, return_value=True), patch(_SLEEP, new_callable=AsyncMock), - caplog.at_level("ERROR"), + # ABORT по доле блоков -- штатный исход скрапинга, не инцидент; понижено + # до warning, чтобы не шуметь в GlitchTip. + caplog.at_level("WARNING"), ): result = await run_avito_detail_backfill( db, run_id=110, params={"batch_size": total, "budget_sec": 3600} @@ -1396,7 +1400,9 @@ async def test_backfill_safety_net_abort_log_names_the_streak(caplog: Any) -> No patch(_FETCH, mock_fetch), patch(_SAVE, return_value=True), patch(_SLEEP, new_callable=AsyncMock), - caplog.at_level("ERROR"), + # ABORT по safety-net серии -- штатный исход скрапинга, не инцидент; + # понижено до warning, чтобы не шуметь в GlitchTip. + caplog.at_level("WARNING"), ): result = await run_avito_detail_backfill( db, run_id=111, params={"batch_size": total, "budget_sec": 3600} diff --git a/tradein-mvp/backend/tests/tasks/test_domclick_detail_backfill.py b/tradein-mvp/backend/tests/tasks/test_domclick_detail_backfill.py index dc68ad7c..0e100ecc 100644 --- a/tradein-mvp/backend/tests/tasks/test_domclick_detail_backfill.py +++ b/tradein-mvp/backend/tests/tasks/test_domclick_detail_backfill.py @@ -287,7 +287,9 @@ async def test_backfill_abort_log_has_no_qrator_literal(caplog: pytest.LogCaptur patch(_BROWSER_FETCHER, mock_bf_cls), patch(_FETCH, mock_fetch), patch(_SLEEP, new_callable=AsyncMock), - caplog.at_level("ERROR"), + # серия подтверждённых блоков площадки -- штатный исход скрапинга, не + # инцидент; понижено до warning, чтобы не шуметь в GlitchTip. + caplog.at_level("WARNING"), ): await run_domclick_detail_backfill( db, diff --git a/tradein-mvp/backend/tests/test_cian_session.py b/tradein-mvp/backend/tests/test_cian_session.py index 9f293e17..d1c07414 100644 --- a/tradein-mvp/backend/tests/test_cian_session.py +++ b/tradein-mvp/backend/tests/test_cian_session.py @@ -446,8 +446,9 @@ async def test_verify_session_pool_exhausted_returns_source_unavailable_with_err ) -> None: """#2825 fail-closed (#2616): пул scrape_proxies исчерпан для cian (все узлы забанены/нездоровы) — session.get НЕ вызывается (никуда не ходим без egress), - возвращается VERIFY_SOURCE_UNAVAILABLE_SENTINEL, но с ERROR-логом (не warning, - отдельным от обычного network-error пути) — явная деградация, а не проглатывание.""" + возвращается VERIFY_SOURCE_UNAVAILABLE_SENTINEL, с явным логом (не тихое + проглатывание). Штатный исход скрапинга -- warning, не error (было error; + понижено, чтобы не шуметь в GlitchTip).""" from app.services.proxy_egress import ProxyPoolExhaustedError def _raise(source: str) -> str | None: @@ -470,8 +471,8 @@ async def test_verify_session_pool_exhausted_returns_source_unavailable_with_err assert result is VERIFY_SOURCE_UNAVAILABLE_SENTINEL mock_session.get.assert_not_called() - errors = [rec for rec in caplog.records if rec.levelname == "ERROR"] - assert any("пул прокси исчерпан" in rec.message.lower() for rec in errors) + warnings = [rec for rec in caplog.records if rec.levelname == "WARNING"] + assert any("пул прокси исчерпан" in rec.message.lower() for rec in warnings) @pytest.mark.asyncio