"""Точка входа FastAPI приложения.""" import logging from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from api.admin import router as admin_router from api.auth import router as auth_router from api.chat import router as chat_router from api.conferences import router as conferences_router from api.health import router as health_router from api.livekit_webhook import router as livekit_webhook_router from api.metrics import prometheus_latency_middleware from api.metrics import router as metrics_router from api.teams import router as teams_router from api.users import router as users_router from core.config import get_settings from core.db import async_session_maker, engine from core.redis import redis_client from services.instance_settings import InstanceSettingsService, bootstrap_overrides_from_settings def _configure_logging() -> None: """Настроить базовый вывод логов приложения (INFO) в stdout. Без этого root-логгер под uvicorn остаётся без хендлера/на уровне WARNING, и INFO-логи наших модулей (например, `services.email` — ссылка подтверждения email в dev-режиме) никуда не попадают, включая `docker logs`. `logging.basicConfig` идемпотентен: если хендлер на root уже есть (в т.ч. при повторных вызовах `create_app()` в тестах), он не добавляется повторно — дублирования строк не будет. """ logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logging.getLogger().setLevel(logging.INFO) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Бутстрап настроек инстанса из `plugins.yaml` + освобождение движка БД и Redis при завершении. Бутстрап (`InstanceSettingsService.ensure_bootstrapped`) идемпотентен (`INSERT ... ON CONFLICT DO NOTHING`) — безопасен при каждом рестарте backend, уже сделанные администратором правки настроек не перетираются. `overrides` — матрица «пресет → настройки» инсталлятора (`BOOTSTRAP_*` в `.env`): влияет только на чистую БД (первый запуск), см. докстринг `ensure_bootstrapped`. """ settings = get_settings() overrides = bootstrap_overrides_from_settings(settings) async with async_session_maker() as session: await InstanceSettingsService(session).ensure_bootstrapped( settings.plugins_config_path, overrides ) try: yield finally: await engine.dispose() await redis_client.aclose() def create_app() -> FastAPI: """Построить и настроить FastAPI приложение.""" _configure_logging() app = FastAPI(title="VidConf API", lifespan=lifespan) # Латентность HTTP по маршрутам — оборачивает # ВСЕ запросы, включая сам /metrics (шаблон пути резолвится по # `request.app.routes` в момент запроса, а не при регистрации — порядок # относительно `include_router` ниже значения не имеет). app.middleware("http")(prometheus_latency_middleware) app.include_router(health_router) app.include_router(metrics_router) app.include_router(auth_router) app.include_router(users_router) app.include_router(teams_router) app.include_router(conferences_router) app.include_router(chat_router) app.include_router(admin_router) app.include_router(livekit_webhook_router) # Статика загруженных файлов (аватары) — в проде эту же # директорию раздаёт nginx (`location /media/`, `deploy/nginx/nginx.conf`), # здесь — dev-режим/fallback. Каталог создаётся заранее: `StaticFiles` # падает при монтировании несуществующей директории. media_root = Path(get_settings().media_root) media_root.mkdir(parents=True, exist_ok=True) app.mount("/media", StaticFiles(directory=media_root), name="media") return app app = create_app()