import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.sessions import SessionMiddleware import sentry_sdk from sentry_sdk.integrations.fastapi import FastApiIntegration import os import sys from app.api.api_router import api_router from app.config import settings from app.core.api_config import API_METADATA, API_TAGS from app.auth import fastapi_users, auth_backend from app.schemas.user import UserPublic, UserCreate, UserUpdate from app.core.scheduler import init_scheduler, shutdown_scheduler from app.core.middleware import RequestContextMiddleware from app.core.logging_utils import PiiRedactionFilter from app.core.error_handlers import sqlalchemy_exception_handler, generic_exception_handler from app.core.rate_limiter import RateLimitMiddleware if settings.SENTRY_DSN: sentry_sdk.init( dsn=settings.SENTRY_DSN, integrations=[ FastApiIntegration(), ], traces_sample_rate=0.1 if settings.is_production else 1.0, environment=settings.ENVIRONMENT, send_default_pii=not settings.is_production ) logging.basicConfig( level=getattr(logging, settings.LOG_LEVEL), format=settings.LOG_FORMAT ) # Attach PII redaction filter to root logger root_logger = logging.getLogger() root_logger.addFilter(PiiRedactionFilter()) logger = logging.getLogger(__name__) api_metadata = { **API_METADATA, "docs_url": settings.docs_url, "redoc_url": settings.redoc_url, "openapi_url": settings.openapi_url, } app = FastAPI( **api_metadata, openapi_tags=API_TAGS ) app.add_middleware( SessionMiddleware, secret_key=settings.SESSION_SECRET_KEY ) # Structured logging & request tracing app.add_middleware(RequestContextMiddleware) app.add_middleware(RateLimitMiddleware) app.add_middleware( CORSMiddleware, allow_origins=(settings.cors_origins_list if not settings.is_production else [settings.FRONTEND_URL]), # Credentials (cookies) are not required because we use JWTs in Authorization headers. allow_credentials=False, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"] ) # Register exception handlers BEFORE adding middleware/router app.add_exception_handler(Exception, generic_exception_handler) from sqlalchemy.exc import SQLAlchemyError app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler) app.include_router(api_router, prefix=settings.API_PREFIX) @app.get("/health", tags=["Health"]) async def health_check(): """Minimal health check endpoint that avoids leaking build metadata.""" return {"status": settings.HEALTH_STATUS_OK} @app.get("/", tags=["Root"]) async def read_root(): """Public root endpoint with minimal information.""" logger.info("Root endpoint '/' accessed.") return {"message": settings.ROOT_MESSAGE} async def run_migrations(): """Run database migrations.""" try: logger.info("Running database migrations...") base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) alembic_path = os.path.join(base_path, 'alembic') if alembic_path not in sys.path: sys.path.insert(0, alembic_path) from migrations import run_migrations as run_db_migrations await run_db_migrations() logger.info("Database migrations completed successfully.") except Exception as e: logger.error(f"Error running migrations: {e}") raise @app.on_event("startup") async def startup_event(): """Initialize services on startup.""" logger.info(f"Application startup in {settings.ENVIRONMENT} environment...") # await run_migrations() init_scheduler() logger.info("Application startup complete.") @app.on_event("shutdown") async def shutdown_event(): """Cleanup services on shutdown.""" logger.info("Application shutdown: Disconnecting from database...") shutdown_scheduler() # Close Redis connection pool to avoid leaking file descriptors. try: from app.core.redis import redis_pool await redis_pool.aclose() logger.info("Redis pool closed.") except Exception as e: logger.warning(f"Error closing Redis pool: {e}") logger.info("Application shutdown complete.")