Skip to content

Приложение к ТЗ: события, ошибки, сообщения, логирование

Дополняет основное ТЗ (§7 Статусы, §8 События, §11 Ошибки). Здесь — исчерпывающий каталог: что может пойти не так, как это классифицируется, что видит пользователь и как это отлаживается.


1. Нормализация и опечатки в артикулах

Артикул — «грязный» ввод (копипаст из Excel/1С/писем). Поиск должен прощать типовые искажения. Пайплайн нормализации применяется к каждой строке столбца A перед запросом к API; исходное и нормализованное значения сохраняются оба.

1.1. Пайплайн нормализации (normalize_code)

ШагЧто делаетПример
trimубрать пробелы по краям и внутри" 0451 103 316 "0451103316
unify-caseпривести к верхнему регистру для сравнения/дедупаbp1234aBP1234A
collapse-spacesсхлопнуть множественные пробелы, затем убратьABC 123ABC123

⚠️ Кириллица сохраняется в основном коде (изменено 2026-07-10). Многие РФ-каталоги (ГАЗ/КамАЗ/ЗМЗ) хранят артикулы в кириллице (напр. 130У.09.012), и принудительная латинизация промахивалась бы по базе. Латинизированный вариант (СC, КK, …) пробуется как кандидат, а не подменяет основной код.

Помимо основного кода строятся кандидаты (для «прощающего» поиска, §1.2):

КандидатНазначение
латинизированный (СC, АA, ОO, КK, УY, …)импортный код, ошибочно введённый кириллицей
без разделителей (-, ., /, \)GDB-1330GDB1330
O↔0, I↔1визуально похожие цифры/буквы

1.2. Стратегия «прощающего» поиска

1. Определение бренда перебирает варианты кода по приоритету
   (оригинал-с-кириллицей → латинизированный → без разделителей → O/0 → I/1),
   но не более settings.FUZZY_MAX_TRIES+1 запросов. Первый вариант, давший бренд,
   и есть рабочий код детали.
2. По найденному коду тянутся entities/relations/cars.
3. Если данных нет и включён fuzzy-режим — добираем оставшихся кандидатов
   (не более settings.FUZZY_MAX_TRIES). В note фиксируется исправленный вариант.
4. Все варианты исчерпаны → not_found.

1.3. Предупреждения о вводе (не блокируют, показываются оператору)

ПроверкаСообщение оператору
пустая строка среди списка«Строка N пустая — пропущена»
длина < 3 или > 250 (лимит API)«Строка N: артикул слишком короткий/длинный — пропущен»
дубликат в списке«Строка N — дубль строки M, запрошено один раз»
очень много строк (> MAX_ROWS)«Список превышает лимit {MAX_ROWS}; лишние строки не приняты»
смешанные разделители/формат«Строка N: необычный формат артикула, проверьте результат»

Предупреждения возвращаются при создании задания (POST /jobswarnings[]) и дублируются в UI до старта обработки.

2. Полный каталог результатов строки

Расширяет §7.2 основного ТЗ. Для КАЖДОЙ строки фиксируется исход и человекочитаемое note. UI и Excel показывают статус + note.

СтатусУсловиеnote (пример)Цвет в Excel/UI
okнайдены все целевые полябез заливки / зелёный бейдж
partialесть данные, но нет части размеров/веса«Нет высоты и веса в базе»жёлтый
partialнайдено по исправленному коду«Найдено по 0451103316 (исправлен формат)»жёлтый
not_foundнет ни entities/relations/cars«Артикул не найден в базе PartsIndex»серый
not_foundбренд не определён«Не удалось определить бренд по артикулу»серый
ambiguousнесколько брендов, политика operator без бренда«Найдено {n} брендов — уточните бренд»оранжевый
errorсетевая/серверная ошибка по строке (после ретраев)«Временная ошибка сервиса, повторите»красный
skippedне прошла валидацию ввода (§1.3)«Пропущена: пустая строка»серый (курсив)

Итоговая сводка задания: {total, ok, partial, not_found, ambiguous, error, skipped}.

3. Каталог технических ошибок (внешний API и инфраструктура)

Единый маппинг «сырой ошибки» → внутренний класс → реакция → сообщение. Внутренние детали (коды, тела) идут в логи; пользователю — только дружелюбный текст.

ИсточникВнутренний кодКлассРеакцияСообщение пользователю
API 200, пустой списокPI_EMPTYdataстрока not_found«Артикул не найден»
API 400PI_BAD_REQUESTclientстрока error, лог WARN«Некорректный запрос по строке»
API 401 no authPI_NO_AUTHconfigJob blocked (no_key), алерт«Сервис данных недоступен (авторизация)»
API 403/1003 ipPI_IP_DENYconfigJob blocked (ip), алерт«Сервис данных временно недоступен»
API 403/1003 quotaPI_QUOTAquotaJob blocked (quota), алерт«Достигнут дневной лимит запросов, задание приостановлено»
API 403/1003 domain/resourcePI_ACCESS_DENYconfigJob blocked, алерт«Сервис данных временно недоступен»
API 404PI_NOT_FOUNDdataстрока not_found«Артикул не найден»
API 429 (если появится)PI_RATEretriablebackoff + ретрай(прозрачно)
API 5xxPI_SERVERretriableретрай до N, затем строка error«Временная ошибка сервиса»
таймаут/сетьPI_TIMEOUTretriableретрай до N, затем строка error«Превышено время ожидания»
невалидный JSON ответаPI_PARSEbugстрока error, лог ERROR + raw«Ошибка обработки ответа»
БД недоступнаDB_DOWNinfra503, задание остаётся running/восстановление«Сервис временно недоступен»
Redis недоступенCACHE_DOWNinfra-softдеградация: без кэша, WARN(прозрачно, но лог)
диск/запись xlsxFILE_IOinfraExcel error, ретрай генерации«Не удалось сформировать файл, повторите»
ключ не задан в .envNO_KEY_CONFIGconfigстарт задания запрещён«Сервис не настроен, обратитесь к администратору»
необработанное исключение в воркереINTERNAL_ERRORbugJob failed, block_reason сохраняется«Внутренняя ошибка обработки задания. Нажмите «Возобновить» или обратитесь к администратору, если повторится.»

Классы и общая политика:

  • retriable — экспоненциальный backoff (напр. 0.5→1→2 с, джиттер), до attempts_max.
  • quota/config — приостановка задания (blocked), не потеря строк; возобновление после сброса лимита/исправления ключа/IP (POST /jobs/{id}/retry).
  • data — нормальный «нет данных», не ошибка; строка not_found.
  • bug/infra — логируются с полным контекстом (см. §5), пользователю — обобщённо.

INTERNAL_ERROR — единственный код класса bug, который переводит всё задание в failed (а не только строку в error): его ставит app/workers/runner.py при необработанном исключении в process_job, чтобы задание не зависало в running до перезапуска воркера. И для blocked (config/quota), и для failed (internal_error) причина пишется в один и тот же Job.block_reason; человекочитаемый текст для обоих случаев отдаёт app/core/errors.error_user_message() — на его основе JobSummary.block_reason_message показывается и в списке заданий (/history), и на странице конкретного задания, без необходимости открывать задание, чтобы понять причину.

Отдельно от этого каталога (который управляет статусом заданий) есть более подробная, специально для UI API-ключей классификация в app/services/api_keys.py: каждый результат живой проверки ключа несёт who (self/supplier/developer/wait/none) — явную подсказку, кому решать проблему — и различает настоящий таймаут (error_kind="timeout") и обрыв сети/DNS/TLS (error_kind="network"), которые раньше обе схлопывались в «не ответил вовремя». Эта классификация показывается в admin → «API-ключи» и через GET /status — операторам (см. §6.4 ниже).

4. Сообщения и статусы для пользователя (UX)

4.1. Принципы

  • Пользователю — человеческий русский текст, без кодов и стектрейсов.
  • Технический код (PI_QUOTA и т.п.) и correlation_id — в свёрнутой «деталке» для поддержки/админа и всегда в логах.
  • Статусы — цветными бейджами + иконкой + подсказкой (tooltip = note).

4.2. Уровни сообщений

УровеньГдеПример
toast successпосле действия«Задание #123 создано, обрабатываем…»
toast warningпредупреждения ввода«3 строки пропущены (пустые/дубли)»
inline bannerсостояние задания«Задание приостановлено: дневной лимит. Возобновить?»
row noteпо строке«Найдено по исправленному коду»
error toastсбой действия«Не удалось скачать файл. Попробуйте ещё раз»

4.3. Отображение прогресса и итога

  • Прогресс-бар done/total + счётчики по статусам вживую (SSE progress).
  • Живая таблица: строки заполняются по мере item.done, с фильтрами (Все / Найдено / Частично / Не найдено / Ошибки).
  • Итоговая плашка: «Готово: {ok} найдено, {partial} частично, {not_found} не найдено, {error} с ошибками» + кнопка «Скачать Excel».
  • blocked → баннер с причиной и кнопкой «Возобновить»; данные уже полученных строк доступны.

4.4. Экспорт «проблемных» строк

Отдельная выгрузка только not_found/error/ambiguous (для повторного прогона после уточнения бренда/исправления) — кнопка «Скачать ненайденные».

5. Логирование

5.1. Формат и транспорт

  • structlog, JSON-логи в stdout (собираются Docker/агрегатором). Не print().
  • Обязательные поля каждого события: ts, level, logger, event, correlation_id, user_id?, job_id?, row_no?, endpoint?, duration_ms?.
  • correlation_id (request id) генерируется на входящий HTTP-запрос (middleware), прокидывается в воркер и во все вызовы PartsIndex; возвращается клиенту в заголовке X-Request-Id и показывается в «деталке» ошибки.

5.2. Уровни

УровеньЧто пишем
DEBUGтела запросов/ответов PartsIndex (усечённо), кандидаты нормализации, cache hit/miss — только при DEBUG=true
INFOстарт/финиш задания, сводка, cache-hit-rate, расход квоты
WARNINGретраи, деградации (Redis down), пропуски строк, PI_BAD_REQUEST
ERRORPI_PARSE, FILE_IO, необработанные исключения, blocked-переходы
CRITICALсервис не может работать (нет ключа, БД недоступна на старте)

5.3. Что НЕ логируем

  • Полный API-ключ (только префикс, напр. OEM-API-F1B2… → маскирование).
  • Пароли/хеши, JWT целиком.
  • Персональные данные, если появятся.

5.4. Метрики (для /admin)

  • Расход квоты по дням/эндпоинтам (api_usage), cache-hit-rate.
  • Кол-во заданий по статусам, среднее время обработки строки, доля not_found.
  • Топ ненайденных артикулов (для анализа качества базы/ввода).

6. Отладка и дебаг

6.1. Режим разработки без боевого ключа

  • Mock-режим PartsIndex-клиента (PI_MODE=mock): фикстуры ответов (entities/relations/cars/brands) из tests/fixtures/ — позволяет разрабатывать всю логику, пока нет ключа/whitelisted-IP.
  • Record/replay: при наличии ключа — записать реальные ответы в фикстуры (PI_MODE=record) и переигрывать (PI_MODE=replay) без расхода квоты.

6.2. Инструменты диагностики

ИнструментНазначение
X-Request-Id в ответе + логахсквозная трассировка одного запроса
raw_json в job_itemsпосмотреть точный ответ API по проблемной строке
GET /jobs/{id}/events (история из job_events)воспроизвести ход выполнения
GET /admin/auditкто что запускал/отменял
GET /health (без авторизации)живы ли БД/Redis, задан ли ключ PartsIndex в .env
GET /status (любой авторизованный)живой статус ключей источников + расход за сегодня
GET /admin/api-keys + POST /admin/api-keys/{source}/check (только admin)маскированный ключ, живая проверка, правка
«деталка» ошибки в UI (код + correlation_id)связать жалобу пользователя с логом

6.3. Health-check GET /health

Без авторизации, только базовая доступность (не бьёт по внешнему API). Реальный формат (app/routers/health.py):

json
{
  "status": "ok|degraded|down",
  "db": "ok|down", "redis": "ok|down",
  "partsindex_key": "configured|missing",
  "partsindex_mode": "live|mock|record|replay",
  "version": "0.1.0"
}

partsindex_key смотрит только .env (settings.pi_api_key) — если ключ задан через админку и хранится в БД (app_settings), а .env пуст, /health всё равно покажет missing. Это ожидаемо: /health — про доступность инфраструктуры для оркестратора/мониторинга, а не про то, работает ли текущий ключ у поставщика.

6.4. Живой статус ключей — GET /status

Для «работает ли ключ прямо сейчас» есть отдельный, более информативный эндпоинт (app/routers/status.py), доступный любому авторизованному пользователю, не только admin:

json
{
  "sources": [
    {"source": "pi", "title": "Parts.Index", "ok": true, "status": "ok",
     "who": null, "who_label": null, "message": "Ключ работает: сервис ответил успешно."},
    {"source": "pc", "title": "Parts-Catalogs (Tradesoft)", "ok": false, "status": "access_deny",
     "who": "supplier", "who_label": "Обратитесь к поставщику данных…", "message": "…"}
  ],
  "quota": [ {"source": "pi-info", "title": "…", "used": 16, "quota": null, "percent": null, "level": "ok"},  ]
}

Ключ (даже маскированный) сюда не попадает — только статус и текст. Результат идёт из api_keys.cached_probe() (кэш ~180 c, чтобы каждый заход оператора не слал лишний запрос поставщику); ручная проверка в admin → «API-ключи» обновляет тот же кэш принудительно. На фронте SourceStatusBanner.vue дёргает /status на каждой странице и показывает баннер только когда ok: false у источника или уровень квоты не ok — молчит, если всё в порядке.

6.5. Воспроизводимость инцидента

По жалобе «строка не нашлась»: оператор сообщает № задания и артикул → админ по raw_json/job_events/логам с correlation_id видит: нормализованный код, какие кандидаты пробовались, ответ API, итоговый статус и причину.

7. Сводка полей для трассировки одной строки

row_no · code (исходный) · нормализованный код · кандидаты · выбранный brand · статус каждого из 3 запросов · attempts · итоговый status · note · correlation_id · raw_json. Этого набора достаточно, чтобы объяснить ЛЮБОЙ исход строки без повторного обращения к API.

Внутренняя документация проекта parts-catalogs.com