From 1fbf77f879c89b8ce3b2611b60e12ae28fbb52ac Mon Sep 17 00:00:00 2001 From: Azamat Date: Thu, 26 Mar 2026 21:29:41 +0300 Subject: [PATCH] [feat] setup master from fork with fastapi + otel --- AGENTS.md | 85 +++++++++++ docs/001-composition-root-and-lifetimes.md | 16 ++ docs/002-config-yaml-plus-env.md | 16 ++ docs/003-observability-via-interfaces.md | 17 +++ docs/004-versioned-http-api.md | 16 ++ .../005-fastapi-otel-early-instrumentation.md | 17 +++ pyproject.toml | 2 +- tasks.md | 142 ++++++++++++++++++ 8 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100644 docs/001-composition-root-and-lifetimes.md create mode 100644 docs/002-config-yaml-plus-env.md create mode 100644 docs/003-observability-via-interfaces.md create mode 100644 docs/004-versioned-http-api.md create mode 100644 docs/005-fastapi-otel-early-instrumentation.md create mode 100644 tasks.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..03787a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,85 @@ +# Project Guide + +## Project +- Python clean architecture template +- FastAPI is the current HTTP adapter +- The web layer must stay replaceable +- Repository and usecase instances are created once at startup +- Config comes from YAML plus env into one dataclass tree +- Logs metrics and traces stay behind interfaces and use OTel adapters +- Request logging middleware is used +- Metrics middleware is used +- Custom trace middleware is not used +- API versioning lives under `/api/v1` + +## Structure +- `domain/` core model and domain errors +- `domain/error.py` domain errors +- `domain/user.py` example domain model +- `usecase/` interfaces and usecases +- `usecase/interface.py` repository and observability interfaces +- `usecase/user.py` example user usecase +- `repository/` repository implementations +- `repository/user.py` example in-memory repository +- `adapter/config/` config models and loader +- `adapter/otel/` logger metrics tracer and OTel setup +- `adapter/di/` container and startup wiring +- `adapter/http/fastapi/` app dependencies lifespan middleware routers +- `config/` app YAML and OTel collector config +- `docs/` ADR files +- `tasks.md` task list +- `main.py` local entrypoint + +## Boundaries +- Keep dependency direction inward +- `domain` imports nothing internal +- `usecase` may import `domain` +- `repository` may import `usecase` and `domain` +- `adapter` may import `usecase` and `domain` +- Do not import FastAPI into `domain` or `usecase` +- Do not import OpenTelemetry into `domain` or `usecase` +- Keep HTTP models and middleware inside `adapter/http/fastapi/` + +## Workflow +- Use `tasks.md` for planning +- Do not use Beads +- Do not use `bd` +- Use `uv` for Python commands and dependency management +- Do not create commits on your own +- Work on one task at a time +- Prefer delegation for implementation +- Delegate only one task at a time +- After one task return to the user with result verification and next options +- Wait for the user before the next task commit or fix + +## Makefile +- `make install` install deps with `uv` +- `make run` start the app locally +- `make run-otel` start the app with OTel env +- `make lint` run `ruff` +- `make typecheck` run `mypy` +- `make test` run `pytest` +- `make pre-commit` run lint typecheck test +- `make compose-build` build the containers +- `make compose-up` start app and collector +- `make compose-down` stop the stack +- `make compose-logs` show app and collector logs +- `make compose-ps` show compose status + +## Runtime +- Local run needs `APP_API_TOKEN` and `APP_SIGNING_KEY` +- Base config lives in `config/app.yaml` +- Env overrides come from shell vars or `.env` +- OTel collector config lives in `config/otel-collector.yaml` + +## Style +- Comments are short and have no trailing period +- Error messages are short and have no trailing period +- Use single-word comments when possible +- Use single-word error messages when possible +- Keep names simple +- Keep adapters thin +- Keep `__init__.py` empty +- Prefer explicit wiring over magic +- Do not expand scope without user approval +- Do not `from __future__ import annotations` diff --git a/docs/001-composition-root-and-lifetimes.md b/docs/001-composition-root-and-lifetimes.md new file mode 100644 index 0000000..c3ce3ec --- /dev/null +++ b/docs/001-composition-root-and-lifetimes.md @@ -0,0 +1,16 @@ +# 001 Composition Root and Lifetimes + +Context +- The template must initialize repository and usecase objects once and reuse them across requests. +- FastAPI integration must not leak framework concerns into inner layers. + +Decision +- Keep a single composition root in `adapter/di/container.py`. +- Build repositories, usecases, config, and observability adapters during application startup. +- Store the container in application state and expose instances through thin HTTP dependencies. +- Do not create repository or usecase objects per request. + +Consequences +- Object lifetimes stay explicit and testable. +- Replacing the web framework affects only the HTTP adapter layer. +- Startup failures surface early instead of during request handling. diff --git a/docs/002-config-yaml-plus-env.md b/docs/002-config-yaml-plus-env.md new file mode 100644 index 0000000..42622b6 --- /dev/null +++ b/docs/002-config-yaml-plus-env.md @@ -0,0 +1,16 @@ +# 002 Config From YAML and Env + +Context +- The service needs readable project configuration and safe handling of sensitive values. +- The final configuration object should be easy to inject and test. + +Decision +- Keep non-sensitive defaults in YAML files under `config/`. +- Read sensitive values only from environment variables. +- Merge YAML and env sources into one `AppConfig` dataclass tree in `adapter/config/`. +- Keep configuration parsing and validation outside `domain/` and `usecase/`. + +Consequences +- Local configuration remains simple to inspect and version. +- Secrets stay out of committed files. +- Inner layers depend on typed config data, not on env or YAML readers. diff --git a/docs/003-observability-via-interfaces.md b/docs/003-observability-via-interfaces.md new file mode 100644 index 0000000..c1b5770 --- /dev/null +++ b/docs/003-observability-via-interfaces.md @@ -0,0 +1,17 @@ +# 003 Observability Via Interfaces + +Context +- Logging, metrics, and tracing are required, but inner layers must stay independent from OpenTelemetry. +- The project needs request logging and metrics middleware. + +Decision +- Define observability interfaces in `usecase/interface.py`. +- Implement those interfaces with OpenTelemetry adapters in `adapter/otel/`. +- Use request logging middleware and metrics middleware in the FastAPI adapter. +- Do not add a custom trace middleware by default. +- Use standard ASGI/FastAPI OpenTelemetry instrumentation for request spans. + +Consequences +- Usecases stay portable and framework-agnostic. +- Trace behavior remains consistent with OpenTelemetry defaults. +- Extra span enrichment can be added later without changing inner-layer contracts. diff --git a/docs/004-versioned-http-api.md b/docs/004-versioned-http-api.md new file mode 100644 index 0000000..6656524 --- /dev/null +++ b/docs/004-versioned-http-api.md @@ -0,0 +1,16 @@ +# 004 Versioned HTTP API + +Context +- The template needs explicit API versioning and an HTTP boundary that can be replaced later. +- FastAPI is the initial framework, but it must not become a hard dependency for core logic. + +Decision +- Place HTTP transport code under `adapter/http/fastapi/`. +- Mount routers under `/api/v1` and keep version-specific routers in `routers/v1/`. +- Map HTTP requests to usecase calls through thin dependencies and handlers only. +- Keep framework-specific request and response models in the HTTP adapter layer. + +Consequences +- New API versions can be introduced without changing usecase contracts. +- Replacing FastAPI means rewriting only the HTTP adapter. +- Domain and usecase layers remain free from transport concerns. diff --git a/docs/005-fastapi-otel-early-instrumentation.md b/docs/005-fastapi-otel-early-instrumentation.md new file mode 100644 index 0000000..2c52687 --- /dev/null +++ b/docs/005-fastapi-otel-early-instrumentation.md @@ -0,0 +1,17 @@ +# 005 Early FastAPI OTel Instrumentation + +Context +- HTTP spans and HTTP metrics are provided by FastAPI/ASGI OpenTelemetry middleware. +- Starlette caches `middleware_stack` on first ASGI entry, including lifespan startup. +- Instrumenting FastAPI inside lifespan is too late for the initial middleware stack. + +Decision +- Build the application container before returning the FastAPI app from `create_app`. +- Configure FastAPI OpenTelemetry instrumentation in the app factory, not in lifespan. +- Pass the configured tracer and meter providers directly to `FastAPIInstrumentor.instrument_app(...)`. +- Keep lifespan focused on shutdown and resource cleanup. + +Consequences +- `OpenTelemetryMiddleware` is present in the runtime stack without manual stack rebuilds. +- HTTP traces and HTTP metrics use the same startup wiring as other singleton adapters. +- Observability bootstrap stays explicit in the outer adapter layer. diff --git a/pyproject.toml b/pyproject.toml index 062cb93..b72a3e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "web-python-skelet" +name = "master" version = "0.0.1" description = "" readme = "README.md" diff --git a/tasks.md b/tasks.md new file mode 100644 index 0000000..b50acb2 --- /dev/null +++ b/tasks.md @@ -0,0 +1,142 @@ +# План работ: web-python-skelet + +## Контекст + +- Источник требований: `AGENTS.md` и ADR `docs/001`-`docs/004` +- Текущее состояние: в `adapter/`, `domain/`, `usecase/`, `repository/`, `test/` пока только `__init__.py` +- Отсутствуют рабочие каталоги и файлы из целевой структуры: `adapter/config/`, `adapter/otel/`, `adapter/di/`, `adapter/http/fastapi/`, `config/`, `main.py` +- Ограничения: `docs/` и `tasks.md` не добавлять в git; коммиты не делать; работать по одной задаче +- ADR пока покрывают архитектуру, новые ADR нужны только если по ходу работ изменится решение + +## Правила выполнения + +- Каждую задачу выполнять отдельным заходом, без параллельной реализации +- Каждый субагент отдает diff, список измененных файлов и проверку, но не делает commit +- Если в задаче всплывает архитектурное изменение, остановиться и вынести вопрос на согласование + +## Очередь задач + +### T01. Базовый каркас домена и usecase + +- Исполнитель: `primary-agent` (scaffolding) +- Статус: completed +- Зависимости: нет +- Commit required: no +- Scope: создать базовые файлы и контракты в `domain/`, `usecase/`, `repository/` +- Файлы: `domain/error.py`, `domain/user.py`, `usecase/interface.py`, `usecase/user.py`, `repository/user.py` +- Критерии приемки: зависимости направлены внутрь; в `domain/` и `usecase/` нет FastAPI/OTel; есть пример сущности, ошибок, портов и простого usecase + +### T02. Конфиг из YAML и env + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T01` +- Commit required: no +- Scope: собрать typed-config слой в `adapter/config/` и подготовить базовые yaml-файлы +- Файлы: `adapter/config/*`, `config/app.yaml` +- Критерии приемки: конфиг собирается в одну dataclass-структуру; секреты читаются из env; парсинг и валидация не протекают в inner layers + +### T03. Observability порты и OTel adapter + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T01`, `T02` +- Commit required: no +- Scope: реализовать логгер, метрики, трейсинг и bootstrap OTel в `adapter/otel/` через интерфейсы из `usecase/interface.py` +- Файлы: `adapter/otel/*`, `config/otel-collector.yaml` +- Критерии приемки: inner layers знают только интерфейсы; OTLP exporter настраивается из конфига; нет кастомного trace middleware + +### T04. Composition root и lifetime singleton-объектов + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T01`, `T02`, `T03` +- Commit required: no +- Scope: собрать контейнер и startup wiring в `adapter/di/` +- Файлы: `adapter/di/container.py`, `adapter/di/__init__.py` +- Критерии приемки: repository/usecase создаются один раз на старте; контейнер хранит инстансы явно; нет пересоздания на HTTP-запрос + +### T05. FastAPI adapter как заменяемый web layer + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T04` +- Commit required: no +- Scope: поднять HTTP adapter в `adapter/http/fastapi/` с app factory, lifespan, dependencies, middleware и router ` /api/v1` +- Файлы: `adapter/http/fastapi/*`, `main.py` +- Критерии приемки: FastAPI изолирован в adapter-слое; handlers тонкие; request logging и metrics middleware подключены; usecase/repository берутся из контейнера + +### T06. Локальный runtime и compose-окружение + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T02`, `T03`, `T05` +- Commit required: no +- Scope: добавить контейнерный runtime для сервиса и compose-окружение с OTel и UI для просмотра логов, метрик и трейсов +- Файлы: `Dockerfile`, `docker-compose.yml` +- Ограничения: не трогать репозиторный `config/app.yaml`; docker должен прокидывать свой runtime-config внутрь контейнера; Dockerfile только с двумя стадиями `build` и `run` +- Критерии приемки: `make compose-build` и `make compose-up` опираются на существующие файлы; сервис поднимается в контейнере; OTel telemetry уходит в dockerized stack; есть UI для просмотра логов, метрик и трейсов; для локального docker-окружения достаточно только `Dockerfile` и `docker-compose.yml` + +### T07. Тесты на lifetimes, config и HTTP smoke + +- Субагент: `test-engineer` +- Статус: pending +- Зависимости: `T01`, `T02`, `T03`, `T04`, `T05`, `T06` +- Commit required: no +- Scope: покрыть тестами ключевые архитектурные гарантии +- Файлы: `test/*` +- Критерии приемки: есть тест на singleton lifetime для repository/usecase; есть тест merge YAML+env; есть smoke-тест для ` /api/v1`; тесты не тянут FastAPI/OTel в inner layers + +### T08. Архитектурный и boundary review + +- Субагент: `code-reviewer` +- Статус: pending +- Зависимости: `T07` +- Commit required: no +- Scope: проверить импорты, соблюдение слоев, startup lifetimes и заменяемость web adapter +- Файлы: весь измененный код +- Критерии приемки: dependency direction не нарушен; FastAPI и OTel не протекают в `domain/` и `usecase/`; замечания сформулированы как точечные правки или подтверждение готовности к review + +### T09. Конфигурируемый runtime observability + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T03`, `T04`, `T05` +- Commit required: no +- Scope: сделать конфигурируемый runtime observability с настраиваемым sink и форматом логов, плюс отдельными флагами для метрик и трейсов +- Файлы: `adapter/config/*`, `adapter/observability/*`, `adapter/otel/*`, `adapter/di/*`, `adapter/http/fastapi/*`, `config/app.yaml`, при необходимости `main.py` +- Конфиг: `logging.output=stdout|file|otel`, `logging.format=text|json`, `logging.file_path=...`, `metrics.enabled=true|false`, `tracing.enabled=true|false` +- Решение: вынести выбор runtime в отдельный adapter-layer factory; `domain/` и `usecase/` не менять; для `stdout` и `file` поддержать оба формата `text` и `json`; `logging.file_path` использовать только при `logging.output=file`; при отключенных метриках и трейсах использовать `Noop`-реализации; OTel runtime поднимать только если нужен хотя бы для одного сигнала +- Критерии приемки: при `logging.output=stdout` логи идут в stdout в формате `text` или `json` по конфигу; при `logging.output=file` логи пишутся в файл по пути `logging.file_path` в формате `text` или `json`; при `logging.output=otel` логи уходят в collector; `metrics.enabled=false` отключает метрики и metrics middleware; `tracing.enabled=false` отключает FastAPI instrumentation и tracer runtime; DI продолжает отдавать единый runtime через контейнер; внутренние слои по-прежнему знают только порты + +### T10. ADR: раннее подключение OTel к FastAPI + +- Исполнитель: `primary-agent` (docs) +- Статус: completed +- Зависимости: нет +- Commit required: no +- Scope: зафиксировать правило, что FastAPI OTel instrumentation выполняется до первой сборки `middleware_stack` +- Файлы: `docs/005-fastapi-otel-early-instrumentation.md` +- Критерии приемки: ADR занимает 10-20 строк; описаны context, decision, consequences; решение не переписывает историю прошлых ADR + +### T11. Перенос FastAPI OTel bootstrap в app factory + +- Субагент: `feature-developer` +- Статус: completed +- Зависимости: `T10` +- Commit required: no +- Scope: перенести создание container, установку `FastAPIInstrumentor.instrument_app(...)` из `lifespan` в `create_app`, оставив в `lifespan` только shutdown +- Файлы: `adapter/http/fastapi/app.py`, `adapter/http/fastapi/lifespan.py`, при необходимости `adapter/http/fastapi/dependencies.py` +- Ограничения: не использовать ручной rebuild `app.middleware_stack`; не менять `domain/` и `usecase/`; не добавлять бизнес-логику; сохранить singleton-lifetime container +- Критерии приемки: instrumentation происходит до первой сборки middleware stack; `OpenTelemetryMiddleware` попадает в runtime stack без workaround; shutdown закрывает instrumentation и runtime один раз; compose-конфиг продолжает работать + +### T12. Регрессионная проверка HTTP telemetry wiring + +- Субагент: `test-engineer` +- Статус: pending +- Зависимости: `T11` +- Commit required: no +- Scope: добавить проверку, что раннее instrumentation wiring сохраняется и не требует ручного rebuild middleware stack +- Файлы: `test/*` +- Ограничения: без реального collector; проверять через ASGI/lifespan или локальные assertions по app runtime; не тянуть FastAPI и OTel в inner-layer тесты +- Критерии приемки: тест подтверждает, что при включенных metrics/tracing `OpenTelemetryMiddleware` присутствует в runtime stack; тест не зависит от внешнего OTel collector; существующие архитектурные границы не нарушены