from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from dotenv import load_dotenv from pathlib import Path from datetime import datetime, UTC from dateutil import tz from signing_service.application.settings.container import get_settings from signing_service.application.settings.setup_logger import create_logger from signing_service.api.routes.sign_document import router as sign_router from signing_service.application.settings.version import get_package_version load_dotenv() # 👇 FAIL FAST: load settings at startup settings = get_settings() version = get_package_version() def build_logger(): """Crea el logger de aplicación y asegura el directorio de logs.""" log_dir = Path(settings.state_path) log_dir.mkdir(parents=True, exist_ok=True) app_logger = create_logger( name="factuges-document-signing-service", log_path=log_dir / "factuges-document-signing-service.log", ) app_logger.info("") app_logger.info( "============================================================") app_logger.info("FactuGES Document Signing Service - START") app_logger.info("Version: %s", version) app_logger.info("UTC Now: %s", datetime.now(UTC).isoformat()) app_logger.info("Environment: %s", settings.app_env) app_logger.info("") app_logger.info("Log Level: %s", settings.log_level) app_logger.info( "Log: %s", log_dir / "factuges-document-signing-service.log", ) app_logger.info("") app_logger.info("Secret Provider: %s", settings.secret_provider) app_logger.info("Infisical project ID: %s", settings.infisical_project_id) app_logger.info("Infisical environment: %s", settings.infisical_env_slug) app_logger.info("") return app_logger logger = build_logger() async def http_exception_handler(request: Request, exc: HTTPException): """Registra y devuelve errores HTTP controlados.""" logger.error( "HTTPException %s %s | status=%s | detail=%s", request.method, request.url.path, exc.status_code, exc.detail, ) return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, ) class LoggingMiddleware(BaseHTTPMiddleware): """Registra request y response para trazabilidad básica.""" async def dispatch(self, request: Request, call_next): client_ip = request.client.host if request.client else "unknown" method = request.method url = request.url.path logger.info("Request: %s %s from %s", method, url, client_ip) response = await call_next(request) logger.info( "Response: %s %s returned %s to %s", method, url, response.status_code, client_ip, ) return response @asynccontextmanager async def lifespan(app: FastAPI): """Gestiona el ciclo de vida de la aplicación.""" logger.info("Starting signing service") yield logger.info("Stopping signing service") async def healthcheck() -> dict[str, str]: """Endpoint simple de healthcheck.""" return {"status": "ok"} async def get_version() -> dict[str, str]: """Devuelve la versión publicada del servicio.""" return {"version": version} def create_app() -> FastAPI: app = FastAPI(lifespan=lifespan, title="FactuGES Document Signing Service", version=version) app.add_exception_handler(HTTPException, http_exception_handler) app.add_middleware(LoggingMiddleware) app.include_router(sign_router) logger.info("API routes registered from sign_document router") app.add_api_route("/health", healthcheck, methods=["GET"]) logger.info("Health check endpoint registered at /health") app.add_api_route("/version", get_version, methods=["GET"]) logger.info("Version endpoint registered at /version") return app app = create_app()