import logging import sys from contextlib import asynccontextmanager from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database import get_db, init_db from app.middleware.rate_limiter import RateLimiterMiddleware from app.middleware.security_headers import SecurityHeadersMiddleware from app.routers import auth, documents, decisions, votes, mandates, protocols, sanctuary, websocket from app.routers import public # ── Structured logging setup ─────────────────────────────────────────────── def _setup_logging() -> None: """Configure structured logging based on environment. - Production/staging: JSON-formatted log lines for log aggregation. - Development: human-readable format with colors. """ log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO) if settings.ENVIRONMENT in ("production", "staging"): # JSON formatter for structured logging formatter = logging.Formatter( '{"timestamp":"%(asctime)s","level":"%(levelname)s",' '"logger":"%(name)s","message":"%(message)s"}', datefmt="%Y-%m-%dT%H:%M:%S", ) else: # Human-readable format for development formatter = logging.Formatter( "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", datefmt="%H:%M:%S", ) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.setLevel(log_level) root_logger.handlers.clear() root_logger.addHandler(handler) # Reduce noise from third-party libraries logging.getLogger("uvicorn.access").setLevel(logging.WARNING) logging.getLogger("sqlalchemy.engine").setLevel( logging.INFO if settings.ENVIRONMENT == "development" else logging.WARNING ) _setup_logging() logger = logging.getLogger(__name__) # ── Application lifespan ─────────────────────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): logger.info( "Demarrage %s (env=%s, log_level=%s)", settings.APP_NAME, settings.ENVIRONMENT, settings.LOG_LEVEL, ) await init_db() yield logger.info("Arret %s", settings.APP_NAME) # ── FastAPI application ─────────────────────────────────────────────────── app = FastAPI( title=settings.APP_NAME, description="Plateforme de decisions collectives pour la communaute Duniter/G1", version="0.5.0", lifespan=lifespan, ) # ── Middleware stack ────────────────────────────────────────────────────── # Middleware is applied in reverse order: last added = first executed. # Order: SecurityHeaders -> RateLimiter -> CORS -> Application app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware( RateLimiterMiddleware, rate_limit_default=settings.RATE_LIMIT_DEFAULT, rate_limit_auth=settings.RATE_LIMIT_AUTH, rate_limit_vote=settings.RATE_LIMIT_VOTE, ) app.add_middleware(SecurityHeadersMiddleware) # ── Routers ────────────────────────────────────────────────────────────── app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(documents.router, prefix="/api/v1/documents", tags=["documents"]) app.include_router(decisions.router, prefix="/api/v1/decisions", tags=["decisions"]) app.include_router(votes.router, prefix="/api/v1/votes", tags=["votes"]) app.include_router(mandates.router, prefix="/api/v1/mandates", tags=["mandates"]) app.include_router(protocols.router, prefix="/api/v1/protocols", tags=["protocols"]) app.include_router(sanctuary.router, prefix="/api/v1/sanctuary", tags=["sanctuary"]) app.include_router(websocket.router, prefix="/api/v1/ws", tags=["websocket"]) app.include_router(public.router, prefix="/api/v1/public", tags=["public"]) # ── Health check ───────────────────────────────────────────────────────── @app.get("/api/health") async def health(db: AsyncSession = Depends(get_db)): """Health check endpoint that verifies database connectivity. Returns status "ok" with database connection info if healthy, or status "degraded" if the database is unreachable. """ try: result = await db.execute(text("SELECT 1")) result.scalar() db_status = "connected" except Exception as exc: logger.warning("Health check: base de donnees inaccessible - %s", exc) db_status = "disconnected" overall_status = "ok" if db_status == "connected" else "degraded" return { "status": overall_status, "environment": settings.ENVIRONMENT, "database": db_status, "version": "0.5.0", }