"""Точка входа FastAPI.""" from __future__ import annotations import time import uuid from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager import structlog from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.base import BaseHTTPMiddleware from app.api.v1.router import api_router from app.core.config import settings from app.core.logging import configure_logging, get_logger from app.db.session import engine from app.services.task_queue import close_task_queue log = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: configure_logging() log.info("app_starting", environment=settings.environment, version=app.version) yield await close_task_queue() await engine.dispose() log.info("app_stopped") class RequestContextMiddleware(BaseHTTPMiddleware): """Присваивает каждому запросу идентификатор и пишет структурный access-лог. request_id возвращается заголовком `X-Request-ID`: по нему оператор, столкнувшийся с ошибкой, находится в логах за одну команду. """ async def dispatch( self, request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: request_id = request.headers.get("x-request-id") or uuid.uuid4().hex structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars(request_id=request_id) started = time.perf_counter() try: response = await call_next(request) except Exception: log.exception( "request_failed", method=request.method, path=request.url.path, duration_ms=round((time.perf_counter() - started) * 1000, 2), ) raise duration_ms = round((time.perf_counter() - started) * 1000, 2) # Health-check'и опрашиваются постоянно и засоряют лог. if not request.url.path.startswith("/api/v1/health"): log.info( "request", method=request.method, path=request.url.path, status=response.status_code, duration_ms=duration_ms, ) response.headers["X-Request-ID"] = request_id return response def create_app() -> FastAPI: configure_logging() app = FastAPI( title=settings.project_name, version="0.1.0", description="Фіскалізація замовлень через Checkbox", lifespan=lifespan, # В проде интерактивная документация закрыта: схема API — лишняя # подсказка для того, кто ищет незащищённый эндпоинт. docs_url=None if settings.is_production else "/docs", redoc_url=None if settings.is_production else "/redoc", openapi_url=None if settings.is_production else "/openapi.json", ) app.add_middleware(RequestContextMiddleware) if settings.cors_origins: app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["X-Request-ID"], ) app.include_router(api_router, prefix="/api/v1") return app app = create_app()