This commit is contained in:
Раис Юсупалиев
2026-03-07 12:29:12 +03:00
commit ff319170a2
20 changed files with 2713 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""
Generate spec/index.md from spec/tasks/*.md
Assumptions:
- Each task file starts with YAML-like front matter:
---
id: 001
title: Add endpoint /reports/addons
status: TODO # TODO | DONE
created: 2026-02-01
---
Notes:
- status lives inside each task file (as requested).
- index.md is fully overwritten and marked as generated.
- No external deps (no PyYAML). Supports simple "key: value" scalars.
Usage:
python3 spec/gen_spec_index.py
python3 spec/gen_spec_index.py --check # verify index is up-to-date (CI)
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
ROOT = Path(__file__).resolve().parents[1]
SPEC_DIR = ROOT / "spec"
TASKS_DIR = SPEC_DIR / "tasks"
INDEX_PATH = SPEC_DIR / "index.md"
ALLOWED_STATUS = {"TODO", "DONE"}
@dataclass(frozen=True)
class TaskMeta:
file: Path
id: str
title: str
status: str
created: str # YYYY-MM-DD
FRONT_MATTER_RE = re.compile(r"^---\s*$")
def read_front_matter(md_path: Path) -> Tuple[Dict[str, str], int]:
"""
Returns (meta_dict, end_line_index_exclusive).
If no front matter, returns ({}, 0).
"""
text = md_path.read_text(encoding="utf-8")
lines = text.splitlines()
if not lines or not FRONT_MATTER_RE.match(lines[0]):
return {}, 0
meta: Dict[str, str] = {}
i = 1
while i < len(lines):
if FRONT_MATTER_RE.match(lines[i]):
return meta, i + 1
line = lines[i].strip()
i += 1
if not line or line.startswith("#"):
continue
# Very small YAML subset: key: value
if ":" not in line:
continue
key, val = line.split(":", 1)
key = key.strip()
val = val.strip()
# Strip optional quotes
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
val = val[1:-1]
meta[key] = val
# Started front matter but never closed
raise ValueError(f"{md_path}: front matter starts with '---' but missing closing '---'")
def normalize_id(md_path: Path, meta: Dict[str, str]) -> str:
# Prefer explicit id, else derive from filename prefix like "001-something.md"
if "id" in meta and meta["id"].strip():
return meta["id"].strip()
m = re.match(r"^(\d+)", md_path.stem)
if m:
return m.group(1)
raise ValueError(f"{md_path}: missing 'id' in front matter and filename doesn't start with digits")
def normalize_title(md_path: Path, meta: Dict[str, str], content_start_line: int) -> str:
if "title" in meta and meta["title"].strip():
return meta["title"].strip()
# Fallback: first markdown heading after front matter: "# Title"
text = md_path.read_text(encoding="utf-8")
lines = text.splitlines()
for line in lines[content_start_line:]:
line = line.strip()
if line.startswith("# "):
return line[2:].strip()
raise ValueError(f"{md_path}: missing 'title' in front matter and no '# ' heading found")
def normalize_status(md_path: Path, meta: Dict[str, str]) -> str:
status = meta.get("status", "").strip().upper()
if status not in ALLOWED_STATUS:
raise ValueError(f"{md_path}: invalid or missing 'status' (must be one of: {sorted(ALLOWED_STATUS)})")
return status
def normalize_created(md_path: Path, meta: Dict[str, str]) -> str:
created = meta.get("created", "").strip()
if created:
# Validate YYYY-MM-DD
try:
dt.date.fromisoformat(created)
except ValueError:
raise ValueError(f"{md_path}: invalid 'created' date '{created}' (expected YYYY-MM-DD)")
return created
# Fallback: file mtime (not perfect, but better than failing)
mtime = dt.datetime.fromtimestamp(md_path.stat().st_mtime, tz=dt.timezone.utc).date().isoformat()
return mtime
def load_tasks() -> List[TaskMeta]:
if not TASKS_DIR.exists():
raise FileNotFoundError(f"Tasks directory not found: {TASKS_DIR}")
md_files = sorted(TASKS_DIR.glob("*.md"))
tasks: List[TaskMeta] = []
seen_ids: Dict[str, Path] = {}
for f in md_files:
meta, content_start = read_front_matter(f)
task_id = normalize_id(f, meta)
if task_id in seen_ids:
raise ValueError(f"Duplicate task id '{task_id}': {seen_ids[task_id]} and {f}")
seen_ids[task_id] = f
title = normalize_title(f, meta, content_start)
status = normalize_status(f, meta)
created = normalize_created(f, meta)
tasks.append(TaskMeta(file=f, id=task_id, title=title, status=status, created=created))
# Sort by numeric id if possible
def sort_key(t: TaskMeta):
try:
return (0, int(t.id))
except ValueError:
return (1, t.id)
return sorted(tasks, key=sort_key)
def render_index(tasks: List[TaskMeta]) -> str:
generated_at = dt.datetime.now(tz=dt.timezone.utc).replace(microsecond=0).isoformat()
lines: List[str] = []
lines.append("# Spec Tasks Index")
lines.append("")
lines.append("> ⚠️ This file is generated. Do not edit manually.")
lines.append(f"> Generated at (UTC): `{generated_at}`")
lines.append("")
lines.append("## Tasks")
lines.append("")
lines.append("| ID | Status | Created | Title | File |")
lines.append("|---:|:------:|:-------:|:------|:-----|")
for t in tasks:
rel = t.file.relative_to(ROOT).as_posix()
title = t.title.replace("|", "\\|")
lines.append(f"| {t.id} | {t.status} | {t.created} | {title} | `{rel}` |")
lines.append("")
lines.append("## Summary")
lines.append("")
todo = sum(1 for t in tasks if t.status == "TODO")
done = sum(1 for t in tasks if t.status == "DONE")
lines.append(f"- Total: **{len(tasks)}**")
lines.append(f"- TODO: **{todo}**")
lines.append(f"- DONE: **{done}**")
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="fail if spec/index.md is not up-to-date")
args = parser.parse_args()
tasks = load_tasks()
content = render_index(tasks)
if args.check:
existing = INDEX_PATH.read_text(encoding="utf-8") if INDEX_PATH.exists() else ""
if existing != content:
print("spec/index.md is not up-to-date. Run: python3 spec/gen_spec_index.py", file=sys.stderr)
return 2
return 0
SPEC_DIR.mkdir(parents=True, exist_ok=True)
INDEX_PATH.write_text(content, encoding="utf-8")
print(f"Wrote {INDEX_PATH.relative_to(ROOT)} ({len(tasks)} tasks)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+25
View File
@@ -0,0 +1,25 @@
# Spec Tasks Index
> ⚠️ This file is generated. Do not edit manually.
> Generated at (UTC): `2026-03-07T09:07:45+00:00`
## Tasks
| ID | Status | Created | Title | File |
|---:|:------:|:-------:|:------|:-----|
| 000 | DONE | 2026-02-01 | Task format reference | `spec/tasks/000_template.md` |
| 001 | TODO | 2026-03-07 | Create app skeleton and component configuration | `spec/tasks/001_create_app_skeleton_and_config.md` |
| 002 | TODO | 2026-03-07 | Implement pure domain quote rules | `spec/tasks/002_implement_pure_domain_quote_rules.md` |
| 003 | TODO | 2026-03-07 | Add CDEK provider adapter | `spec/tasks/003_add_cdek_provider_adapter.md` |
| 004 | TODO | 2026-03-07 | Add Redis price cache repository | `spec/tasks/004_add_redis_price_cache_repository.md` |
| 005 | TODO | 2026-03-07 | Implement aggregator service workflow | `spec/tasks/005_implement_aggregator_service_workflow.md` |
| 006 | TODO | 2026-03-07 | Add delivery price controller endpoint | `spec/tasks/006_add_delivery_price_controller.md` |
| 007 | TODO | 2026-03-07 | Add observability correlation and telemetry | `spec/tasks/007_add_observability_correlation.md` |
| 008 | TODO | 2026-03-07 | Add SigNoz to Telegram alerting configuration | `spec/tasks/008_add_signoz_telegram_alerting.md` |
| 009 | TODO | 2026-03-07 | Add local infrastructure stack | `spec/tasks/009_add_local_infra_stack.md` |
## Summary
- Total: **10**
- TODO: **9**
- DONE: **1**
+178
View File
@@ -0,0 +1,178 @@
# 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 # Бэкенд наблюдаемости (трейсы + метрики + логи + алерты)
```
+34
View File
@@ -0,0 +1,34 @@
---
id: 000
title: Task format reference
status: DONE
created: 2026-02-01
---
## Context
В репозитории хранится эталонная задача, которая показывает обязательный формат задач.
## Goal
Дать каноничный пример минимального формата задачи, обязательного для всех файлов в `spec/tasks/`.
## Constraints
- Содержать только требования к формату, без продуктовой реализации.
- Оставаться совместимой с обязательной структурой задач Planner.
- Не добавлять требований к реализации.
## Acceptance criteria
- Front matter содержит `id`, `title`, `status`, `created`.
- Все обязательные секции присутствуют в требуемом порядке.
- Содержимое остаётся эталонной, неисполняемой задачей.
## Definition of Done
- [x] Front matter использует обязательные поля.
- [x] Обязательные секции присутствуют.
- [x] Файл пригоден только как reference формата.
## Tests
- Runtime-тесты не требуются.
- Формат валидируется успешной генерацией spec index.
## Commands
- `python3 spec/gen_spec_index.py --check`
@@ -0,0 +1,38 @@
---
id: 001
title: Create app skeleton and component configuration
status: DONE
created: 2026-03-07
---
## Context
Сейчас в репозитории есть только specification-файлы, при этом `spec/overview.md` задаёт конкретную структуру приложения и требования к конфигурации компонентов.
## Goal
Создать начальный `app/` skeleton и модели конфигурации из project overview, включая отдельные секции конфигурации для Controller, Service, Business Logic, Repository, Adapter, Observability и Alerts.
## Constraints
- Соблюдать layered architecture из `AGENTS.md`.
- Scope задачи: только scaffolding и configuration, без business workflows.
- Не реализовывать provider HTTP calls, cache behavior или aggregation logic в рамках этой задачи.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Структура `app/` создана согласно `spec/overview.md`.
- `app/config.py` содержит отдельные component configuration sections. Config must be a .yaml-file added to .gitignore.
- FastAPI entrypoint существует и успешно импортирует configuration.
- В файлах Controller, Service, Repository и Adapter отсутствуют business rules.
## Definition of Done
- [ ] Созданы обязательные директории `app/` и базовые файлы.
- [ ] Реализованы раздельные секции конфигурации по компонентам.
- [ ] Проходит app import smoke test.
- [ ] Изменения остаются строго в scope задачи.
## Tests
- Добавить/import smoke test для FastAPI app startup.
- Добавить unit tests, проверяющие загрузку configuration sections из environment variables.
## Commands
- `poetry run pytest tests/smoke/test_app_import.py -q`
- `poetry run pytest tests/config/test_config_sections.py -q`
@@ -0,0 +1,37 @@
---
id: 002
title: Implement pure domain quote rules
status: TODO
created: 2026-03-07
---
## Context
`spec/overview.md` требует business rules для фильтрации тарифов, сравнения цен, сортировки и нормализации входных данных в чистом слое Business Logic.
## Goal
Реализовать детерминированные, dependency-free domain functions в `app/domain/quotes.py` для нормализации запроса и фильтрации/упорядочивания котировок.
## Constraints
- Business Logic должна быть pure и dependency-free.
- Запрещены прямые IO, adapters, repositories, framework imports и внешние вызовы.
- Слои Service и Controller не должны забирать domain rules из этой задачи.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Domain functions нормализуют значения запроса по явным детерминированным правилам.
- Domain functions отфильтровывают невалидные котировки по определённым domain conditions.
- Domain functions сортируют валидные котировки по возрастанию цены.
- Логика реализована только в `app/domain/` и доступна для вызова из Service.
## Definition of Done
- [ ] Реализованы pure domain functions для normalization/filtering/sorting.
- [ ] Поведение функций детерминированное и без side effects.
- [ ] В domain module нет импортов не-domain зависимостей.
- [ ] Unit tests (без mocks) покрывают стандартные и edge cases.
## Tests
- Добавить `tests/domain/test_quotes.py` только с unit tests.
- Покрыть edge cases: пустой input, невалидные quotes, одинаковые цены, границы нормализации.
## Commands
- `poetry run pytest tests/domain/test_quotes.py -q`
@@ -0,0 +1,39 @@
---
id: 003
title: Add CDEK provider adapter
status: TODO
created: 2026-03-07
---
## Context
Phase 1 в `spec/overview.md` требует поддержку provider CDEK с OAuth2 authentication и маппингом расчёта тарифа.
## Goal
Реализовать adapter interfaces и модули CDEK adapter: provider contract, OAuth2 token handling, HTTP client behavior и преобразование ответа в `DeliveryPrice`.
## Constraints
- Только Adapter layer; без business decisions и sorting logic.
- Внешний IO должен оставаться внутри adapter modules.
- Timeout для CDEK должен быть 10 секунд согласно specification.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Существует интерфейс `DeliveryProvider` со стабильным контрактом `get_price()`.
- Модуль CDEK auth получает OAuth2 token через client credentials и переиспользует валидный token до истечения.
- CDEK client выполняет tariff request с timeout и retry behavior.
- CDEK mapper преобразует provider response в унифицированную schema `DeliveryPrice`.
- Adapter наружу предоставляет только provider-facing API, необходимый Service.
## Definition of Done
- [ ] Реализован base provider interface.
- [ ] Реализованы модули CDEK auth/client/mapper.
- [ ] CDEK adapter возвращает унифицированную price model при успешном ответе.
- [ ] Adapter tests покрывают auth refresh, timeout/retry и mapping cases.
## Tests
- Добавить adapter unit tests для token lifecycle behavior.
- Добавить adapter tests для response mapping и HTTP error handling.
- Для внешних HTTP взаимодействий использовать stubs/mocks.
## Commands
- `poetry run pytest tests/adapters/delivery_providers/cdek -q`
@@ -0,0 +1,37 @@
---
id: 004
title: Add Redis price cache repository
status: TODO
created: 2026-03-07
---
## Context
Product requirements требуют кеширование ответов provider с configurable TTL, а архитектура закрепляет доступ к данным за слоем Repository.
## Goal
Реализовать `PriceCache` repository на Redis с операциями `get`, `set` и `invalidate`, включая поддержку TTL.
## Constraints
- Только Repository layer: data access и persistence mapping.
- В repository code не допускаются business decisions и workflow orchestration.
- Генерация cache key не должна реализовываться в repository.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Repository module существует по пути `app/repositories/cache/redis_cache.py`.
- `PriceCache` предоставляет async методы `get`, `set`, `invalidate`.
- `set` применяет TTL из configuration.
- Serialization/deserialization cached payload выполняются детерминированно.
## Definition of Done
- [ ] Реализован Redis-backed cache repository.
- [ ] Интерфейс repository соответствует требуемым операциям.
- [ ] Поведение TTL покрыто тестами.
- [ ] В repository отсутствует business logic.
## Tests
- Добавить repository tests для cache hit/miss, set/get roundtrip, invalidate и TTL expiration.
- Использовать Redis test double или изолированный test Redis instance.
## Commands
- `poetry run pytest tests/repositories/cache/test_redis_cache.py -q`
@@ -0,0 +1,39 @@
---
id: 005
title: Implement aggregator service workflow
status: TODO
created: 2026-03-07
---
## Context
`spec/overview.md` определяет `AggregatorService.get_all_prices()` как application orchestrator для параллельных provider calls, graceful degradation, caching и отсортированного унифицированного результата.
## Goal
Реализовать `AggregatorService.get_all_prices(request: DeliveryRequest) -> list[DeliveryPrice]` в `app/services/aggregator.py` с параллельным выполнением, cache coordination и делегированием business rules в domain functions.
## Constraints
- Service выполняет только orchestration; pure business rules остаются в Business Logic.
- Использовать dependency injection для providers и repository.
- Для вызовов providers использовать `asyncio.gather(..., return_exceptions=True)`.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Service вызывает все зарегистрированные providers параллельно.
- Failures/timeouts отдельных providers не валят весь запрос; упавшие providers исключаются.
- Cache проверяется до внешнего provider request и обновляется после успешного provider response.
- Фильтрация/сортировка котировок делегируется domain functions.
- Возвращаемый список котировок унифицирован и отсортирован по цене по возрастанию.
## Definition of Done
- [ ] Метод Service реализован с injected dependencies.
- [ ] Параллельная orchestration использует `return_exceptions=True`.
- [ ] Реализована cache coordination для provider responses.
- [ ] Service tests покрывают full-success, partial-failure, all-failure и cache-hit сценарии.
## Tests
- Добавить service tests со stubs/mocks для adapters и repository.
- Проверить orchestration и исключение неуспешных providers.
- Проверить делегирование sorting/filtering в domain logic.
## Commands
- `poetry run pytest tests/services/test_aggregator.py -q`
@@ -0,0 +1,37 @@
---
id: 006
title: Add delivery price controller endpoint
status: TODO
created: 2026-03-07
---
## Context
API contract требует endpoint `POST /api/v1/delivery/price` с DTO validation и HTTP error mapping, при этом Controller должен вызывать ровно один метод Service.
## Goal
Реализовать request/response schemas и endpoint в `app/controllers/v1/delivery.py`, который валидирует входные данные, делегирует в `AggregatorService.get_all_prices()` и маппит service exceptions в HTTP responses.
## Constraints
- Controller не должен содержать business logic или provider-specific branching.
- На каждый запрос Controller должен вызывать ровно один метод Service.
- Validation должна использовать Pydantic models из schema layer.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- `POST /api/v1/delivery/price` принимает payload `DeliveryRequest` и возвращает `list[DeliveryPrice]`.
- Невалидный input возвращает validation error response.
- Controller делегирует обработку в `AggregatorService.get_all_prices()`.
- Service exceptions маппятся в детерминированные HTTP responses.
## Definition of Done
- [ ] Реализованы request и response schemas.
- [ ] Controller endpoint подключён в FastAPI router.
- [ ] Path и HTTP method endpoint соответствуют specification.
- [ ] API tests покрывают успешный ответ, validation failure и mapped service error.
## Tests
- Добавить controller tests для поведения route и делегирования в service.
- Добавить API-level tests для валидации request schema и response schema.
## Commands
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
@@ -0,0 +1,41 @@
---
id: 007
title: Add observability correlation and telemetry
status: TODO
created: 2026-03-07
---
## Context
Проект требует связать traces, metrics и logs через request correlation (`request_id` и `trace_id`) и добавить manual spans вокруг ключевых операций.
## Goal
Реализовать observability wiring: middleware для `request_id`, propagation structured logging context, OpenTelemetry instrumentation для FastAPI/httpx и manual spans для операций service/provider/cache.
## Constraints
- Все observability concerns держать вне Business Logic.
- При добавлении telemetry не вводить business decision-making.
- Инструментировать только слои и операции, указанные в `spec/overview.md`.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Middleware назначает UUID `request_id` для каждого запроса.
- `request_id` добавляется в structured logs через contextvars.
- Включена OpenTelemetry instrumentation для FastAPI и httpx.
- Есть manual spans для `AggregatorService.get_all_prices`, provider `get_price` и cache operations.
- Span attributes включают `provider`, `from_city`, `to_city`, `weight_kg`, `cache_hit`, `tariffs_found` там, где применимо.
## Definition of Done
- [ ] Реализован middleware корреляции запросов.
- [ ] В logging context есть binding `request_id`.
- [ ] Реализована автоматическая и manual tracing instrumentation.
- [ ] Observability tests проверяют создание spans и обязательные attributes.
## Tests
- Добавить middleware tests для генерации и propagation `request_id`.
- Добавить telemetry tests с in-memory span exporter для проверки names/attributes.
- Добавить logging tests, проверяющие наличие `request_id` в log context.
## Commands
- `poetry run pytest tests/controllers/test_middleware_request_id.py -q`
- `poetry run pytest tests/observability/test_tracing.py -q`
- `poetry run pytest tests/observability/test_logging_context.py -q`
@@ -0,0 +1,41 @@
---
id: 008
title: Add SigNoz to Telegram alerting configuration
status: TODO
created: 2026-03-07
---
## Context
`spec/overview.md` требует anomaly alerting через цепочку `SigNoz Alert Rules -> Webhook -> Telegram Bot API` с вынесением token в configuration.
## Goal
Добавить configuration и infrastructure artifacts, необходимые для маршрутизации anomaly alerts из SigNoz в Telegram и для соответствия alert thresholds требованиям проекта.
## Constraints
- Alert transport/configuration должны быть отделены от business logic.
- Telegram credentials должны приходить из configuration и не быть hardcoded.
- Thresholds должны соответствовать alert conditions из `spec/overview.md`.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Configuration содержит отдельную секцию Alerts с Telegram settings.
- Infrastructure artifact(s) описывают routing от SigNoz alert webhook к Telegram Bot API.
- Представлены alert conditions для:
- provider 5xx: 5 ошибок за 5 минут
- provider p99 latency > 5000ms в течение 10 минут
- provider unavailable более 5 минут
- Setup instructions содержат обязательные environment variables и шаги верификации.
## Definition of Done
- [ ] Реализована schema конфигурации Alerts.
- [ ] Добавлены artifact(s) для маршрутизации SigNoz-to-Telegram.
- [ ] Определены все три обязательных anomaly conditions.
- [ ] Инструкции валидации исполнимы в local environment.
## Tests
- Добавить config tests для проверки загрузки alert settings и обязательных полей.
- Добавить tests для alert payload transformation/transport logic, если он реализован в коде.
## Commands
- `poetry run pytest tests/config/test_alerts_config.py -q`
- `docker compose config`
+39
View File
@@ -0,0 +1,39 @@
---
id: 009
title: Add local infrastructure stack
status: TODO
created: 2026-03-07
---
## Context
Overview задаёт локальные infrastructure components `app`, `redis` и `signoz`, необходимые для runtime и observability.
## Goal
Добавить Docker Compose infrastructure definitions для local development, включая application service, Redis cache и SigNoz observability backend.
## Constraints
- Scope задачи: только infrastructure wiring.
- Не переносить business logic в infrastructure scripts.
- Имена services должны соответствовать overview: `app`, `redis`, `signoz`.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- Compose file определяет services `app`, `redis`, `signoz`.
- Service `app` содержит environment wiring для cache и observability endpoints.
- Service `redis` достижим из `app` во внутренней сети.
- Service `signoz` достижим для telemetry export при локальном запуске.
## Definition of Done
- [ ] Добавлен compose definition с обязательными services.
- [ ] Задокументированы environment variables для локальной интеграции.
- [ ] `docker compose config` проходит валидацию.
- [ ] Добавлены базовые startup instructions.
## Tests
- Добавить/обновить infrastructure checks для валидации compose syntax.
- Добавить smoke sequence команд для проверки локального запуска.
## Commands
- `docker compose config`
- `docker compose up -d redis signoz`
- `docker compose ps`