Files
g2s-aggregator/spec/overview.md
T
Раис Юсупалиев ff319170a2 Init
2026-03-07 13:20:53 +03:00

178 lines
7.7 KiB
Markdown

# spec/overview.md
Контекст, специфичный для проекта агрегатора служб доставки.
Глобальные правила определены в AGENTS.md.
---
## Проект
**Название:** Агрегатор служб доставки (бэкенд)
**Язык:** Python 3.14
**Назначение:** Агрегировать расчёты стоимости доставки от нескольких служб и отдавать единый ответ фронтенду через унифицированный API.
---
## Продуктовые требования
- Принимать запрос на расчёт стоимости доставки (откуда, куда, вес, габариты)
- Опрашивать всех зарегистрированных провайдеров параллельно
- Возвращать унифицированный список тарифов, отсортированных по цене
- Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
- Кешировать ответы провайдеров для исключения повторных внешних запросов. Время кэширования вынести в конфиг
- Обеспечивать структурированную наблюдаемость: трейсы, метрики, логи — связанные по request ID
- Отправлять алерты в Telegram при аномалиях (ошибки, всплески задержки, недоступность провайдера). token вынести в конфиг
---
## Провайдеры
### Фаза 1
- **CDEK** — https://apidoc.cdek.ru/
- Аутентификация: OAuth2 (client credentials)
- Операции: расчёт тарифа
- Cache TTL: 15 минут
### Фаза 2+
- Дополнительные провайдеры (Boxberry, DHL и др.) — подключаются через интерфейс `DeliveryProvider` без изменений в логике агрегации
---
## Компоненты
### Controller (`app/controllers/v1/delivery.py`)
- `POST /api/v1/delivery/price` — принимает `DeliveryRequest`, возвращает `list[DeliveryPrice]`
- Парсинг и валидация входных данных через Pydantic
- Маппинг исключений сервиса в HTTP-ответы
- Вызывает ровно один метод Service: `AggregatorService.get_all_prices()`
### Service (`app/services/aggregator.py`)
- `AggregatorService.get_all_prices(request: DeliveryRequest) -> list[DeliveryPrice]`
- Распределяет запросы по всем зарегистрированным провайдерам через `asyncio.gather(..., return_exceptions=True)`
- Фильтрует упавшие результаты
- Сортирует тарифы по цене
- Не содержит бизнес-логики и логики, специфичной для провайдеров
### Business Logic (`app/domain/`)
- Правила сортировки и фильтрации тарифов
- Логика сравнения цен
- Нормализация входных данных (например, правила округления веса)
- Чистые функции, без IO, без зависимостей от фреймворков
### Repository (`app/repositories/cache/`)
- `PriceCache` — хранилище ключ/значение на базе Redis
- Операции: `get(key)`, `set(key, value, ttl)`, `invalidate(key)`
- Без бизнес-решений; формирование ключа — ответственность Adapter
### Adapter (`app/adapters/delivery_providers`)
- `base.py` — абстрактный интерфейс `DeliveryProvider`:
```python
class DeliveryProvider(ABC):
name: str
async def get_price(self, request: DeliveryRequest) -> DeliveryPrice: ...
```
- `cdek/client.py` — HTTP-клиент (httpx AsyncClient), аутентификация, ретраи, таймаут (10с)
- `cdek/auth.py` — управление OAuth2-токеном
- `cdek/mapper.py` — ответ CDEK → `DeliveryPrice`
- Каждый адаптер владеет своей конфигурацией; наружу экспонирует только `get_price()`
---
## Модели данных
### Входная: `DeliveryRequest`
```
entity: Enum(individual, legal)
from_city: str
to_city: str
weight_kg: float
length_cm: float
width_cm: float
height_cm: float
```
### Выходная: `DeliveryPrice`
```
provider: str
service_name: str
price: Decimal
currency: str
delivery_days_min: int
delivery_days_max: int
```
---
## Технологический стек
| Задача | Инструмент |
|---|---|
| Web-фреймворк | FastAPI |
| HTTP-клиент | httpx (async) |
| Валидация | Pydantic v2 |
| Кеш | Redis |
| Конфигурация | pydantic-settings |
| Сервер | Uvicorn |
| Инструментация | OpenTelemetry SDK |
| Бэкенд наблюдаемости | SigNoz (self-hosted) |
| Структурные логи | structlog |
| Алерты | SigNoz Alert Rules → Webhook → Telegram Bot API |
---
## Наблюдаемость
- Каждому запросу присваивается `request_id` (UUID) через middleware
- `request_id` привязывается ко всем логам через `structlog.contextvars`
- Трейсы отправляются через OpenTelemetry; FastAPI и httpx инструментируются автоматически
- Ручные спаны оборачивают: `AggregatorService.get_all_prices`, `get_price` каждого провайдера, обращения к кешу
- Атрибуты спанов: `provider`, `from_city`, `to_city`, `weight_kg`, `cache_hit`, `tariffs_found`
- Отслеживаемые метрики: количество запросов, количество ошибок, время ответа (p50/p99), доступность каждого провайдера
- Все три сигнала (трейсы, метрики, логи) связаны через `trace_id`
### Условия алертов (Telegram)
- Провайдер вернул 5xx — порог: 5 ошибок за 5 минут
- p99 времени ответа провайдера > 5000ms в течение 10 минут
- Провайдер недоступен более 5 минут
---
## Структура проекта
```
app/
├── controllers/
│ ├── http_client.py
│ ├── middleware.py
│ └── v1/
│ └── delivery.py # Controller
├── services/
│ └── aggregator.py # Service
├── domain/
│ └── quotes.py # Business Logic
├── adapters/
│ └── delivery_providers/
│ ├── base.py # Интерфейс Adapter
│ └── cdek/
│ ├── client.py
│ ├── auth.py
│ └── mapper.py
├── repositories/
│ └── cache/
│ └── redis_cache.py # Repository
├── schemas/
│ ├── request.py
│ └── response.py
└── config.py
```
---
## Инфраструктура
```yaml
# docker-compose сервисы
app # FastAPI-приложение
redis # Кеш тарифов
signoz # Бэкенд наблюдаемости (трейсы + метрики + логи + алерты)
```