[feat] add guides

This commit is contained in:
Azamat 2026-03-26 21:56:10 +03:00
parent 1fbf77f879
commit c5d5e243c1
7 changed files with 836 additions and 43 deletions

216
docs/PROJECT_GUIDE_RU.md Normal file
View file

@ -0,0 +1,216 @@
# Как эта архитектура реализована в проекте
## Общая схема
Проект собирается так:
1. `adapter/config/loader.py` читает YAML, `.env` и env vars
2. `adapter/di/container.py` строит единый контейнер
3. `adapter/observability/factory.py` создает logger, metrics и tracer runtime
4. repository и usecase создаются один раз при старте
5. `adapter/http/fastapi/app.py` создает FastAPI app и подключает router + middleware
Это и есть composition root: все внешние зависимости собираются в одном месте и потом передаются внутрь.
## Как здесь реализована чистая архитектура
### `domain/`
- хранит сущности и доменные ошибки
- не зависит от framework и SDK
### `usecase/`
- описывает сценарии приложения
- задает интерфейсы через `Protocol`
- работает только с абстракциями
### `repository/`
- реализует интерфейсы доступа к данным
- может использовать tracer, clients, DB drivers
### `adapter/`
- оборачивает внешние инструменты
- переводит HTTP/config/OTel в контракты приложения
## Как создать новый use case
Рабочая последовательность:
1. Если нужна новая бизнес-сущность или ошибка, добавить ее в `domain/`
2. Если use case зависит от внешнего ресурса, описать контракт в `usecase/`
4. Создать класс use case
5. Внедрить зависимости через конструктор
6. Подключить use case в `adapter/di/container.py`
Минимальный шаблон:
```py
from usecase.interface import Logger, Metrics, Tracer, UserRepository
from domain.user import User
class CreateUser:
def __init__(self, repository: UserRepository, logger: Logger, metrics: Metrics, tracer: Tracer) -> None:
self._repository = repository
self._logger = logger
self._metrics = metrics
self._tracer = tracer
def execute(self, email: str) -> User:
with self._tracer.start_span('usecase.create_user', attrs={'user.email': email}):
user = self._repository.save(email)
self._metrics.increment('user.create.total')
self._logger.info('user_create', attrs={'user.email': email})
return user
```
Правила:
- не импортировать FastAPI в use case
- не читать env vars в use case
- не работать с `Request`, `Response`, `HTTPException`
## Как работать с repository
### 1. Сначала описать интерфейс
Контракт должен жить рядом с use case, который его использует.
```py
class UserRepository(Protocol):
def get(self, user_id: str) -> User | None: ...
def save(self, user: User) -> None: ...
```
### 2. Потом сделать реализацию
Реализация уходит во внешний слой, например в `repository/`.
```py
class InMemoryUserRepository(UserRepository):
def __init__(self, tracer: Tracer) -> None:
self._tracer = tracer
self._users: dict[str, User] = {}
```
### 3. Подключить в контейнере
Именно `adapter/di/container.py` решает, какую реализацию дать use case.
Это важно: use case не должен сам выбирать repository.
## Как добавить новую HTTP ручку
Последовательность:
1. Создать или переиспользовать use case
2. При необходимости добавить схемы в `adapter/http/fastapi/schemas.py`
3. Добавить dependency, если нужен новый use case из контейнера
4. Добавить маршрут в `adapter/http/fastapi/routers/v1/router.py`
5. Преобразовать HTTP input в команду или query use case
6. Преобразовать результат use case в response model
7. Смаппить доменные ошибки в `HTTPException`
Шаблон:
```py
@router.post('/users', response_model=UserResponse)
def create_user(payload: CreateUserRequest, usecase: CreateUser = Depends(get_create_user)) -> UserResponse:
try:
user = usecase.execute(payload.email)
except UserConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return UserResponse(id=user.id, email=user.email)
```
Правило: router остается тонким.
## Как пользоваться логгером, метриками и трейсами
### Логгер
- использовать для бизнес-событий и предупреждений
- передавать данные через `attrs`
- сообщения делать короткими и стабильными
### Метрики
- использовать для счетчиков и длительностей
- не создавать условные ветки `if metrics_enabled`
- runtime сам подставит рабочую или noop-реализацию
### Трейсы
- открывать span на границе важной операции
- класть ключевые атрибуты в `attrs`
- ошибки писать через `record_error`, если это делает смысл явным
## Как конфигурировать проект
### Базовая конфигурация
Основной файл - `config/app.yaml`.
Там лежат:
- имя приложения
- окружение
- HTTP host/port
- режим логирования
- флаги метрик и трейсов
- OTel endpoints
- имя заголовка токена
### Секреты
Обязательные секреты берутся из env:
- `APP_API_TOKEN`
- `APP_SIGNING_KEY`
### Переопределение через env
Можно переопределять YAML через переменные окружения, например:
- `APP_HTTP_HOST`
- `APP_HTTP_PORT`
- `APP_LOGGING_LEVEL`
- `APP_LOGGING_OUTPUT`
- `APP_LOGGING_FORMAT`
- `APP_LOGGING_FILE_PATH`
- `APP_METRICS_ENABLED`
- `APP_TRACING_ENABLED`
- `APP_OTEL_LOGS_ENDPOINT`
- `APP_OTEL_METRICS_ENDPOINT`
- `APP_OTEL_TRACES_ENDPOINT`
### Локальный запуск
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run
```
### Запуск с OTel
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run-otel
```
### Docker Compose
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make compose-up
```
## Короткая памятка
- новая бизнес-логика -> `usecase/`
- новый контракт -> рядом с use case
- новая реализация контракта -> `repository/` или внешний `adapter/`
- новый HTTP маршрут -> `adapter/http/fastapi/`
- новый infra/runtime code -> `adapter/`
- зависимости связывать только в `adapter/di/container.py`