Merge pull request 'Ожидаемые исходы скрапинга перестают быть ошибками' (#3488) from fix/3471-scraper-log-levels into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-browser (push) Successful in 43s
Deploy Trade-In / build-frontend (push) Successful in 2m32s
Deploy Trade-In / test (push) Successful in 6m37s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 2m5s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m41s

This commit is contained in:
lekss361 2026-09-12 11:48:44 +00:00
commit 994eb79323
13 changed files with 39 additions and 26 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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): отказ, БЕЗ обхода через "

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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:

View file

@ -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}

View file

@ -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,

View file

@ -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