""" Geutebruck Cross-Switching API FastAPI application entry point """ from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError import structlog import sys import sqlalchemy as sa from datetime import datetime from pathlib import Path # Add src/api to Python path for imports sys.path.insert(0, str(Path(__file__).parent)) from config import settings # Configure structured logging structlog.configure( processors=[ structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_log_level, structlog.processors.JSONRenderer() if settings.LOG_FORMAT == "json" else structlog.dev.ConsoleRenderer() ], wrapper_class=structlog.stdlib.BoundLogger, context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), ) logger = structlog.get_logger() # Create FastAPI app app = FastAPI( title=settings.API_TITLE, version=settings.API_VERSION, description="REST API for Geutebruck GeViScope/GeViSoft Cross-Switching Control", docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json" ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Global exception handlers @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Handle validation errors""" logger.warning("validation_error", errors=exc.errors(), body=exc.body) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": "Validation Error", "detail": exc.errors(), }, ) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """Handle unexpected errors""" logger.error("unexpected_error", exc_info=exc) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": "Internal Server Error", "message": "An unexpected error occurred" if settings.ENVIRONMENT == "production" else str(exc), }, ) # Startup event @app.on_event("startup") async def startup_event(): """Initialize services on startup""" logger.info("startup", api_title=settings.API_TITLE, version=settings.API_VERSION, environment=settings.ENVIRONMENT) # Initialize Redis connection try: from clients.redis_client import redis_client await redis_client.connect() logger.info("redis_connected", host=settings.REDIS_HOST, port=settings.REDIS_PORT) except Exception as e: logger.error("redis_connection_failed", error=str(e)) # Non-fatal: API can run without Redis (no caching/token blacklist) # Initialize gRPC SDK Bridge client try: from clients.sdk_bridge_client import sdk_bridge_client await sdk_bridge_client.connect() logger.info("sdk_bridge_connected", url=settings.sdk_bridge_url) except Exception as e: logger.error("sdk_bridge_connection_failed", error=str(e)) # Non-fatal: API can run without SDK Bridge (for testing) # Database connection pool is initialized lazily via AsyncSessionLocal logger.info("startup_complete") # Shutdown event @app.on_event("shutdown") async def shutdown_event(): """Cleanup on shutdown""" logger.info("shutdown") # Close Redis connections try: from clients.redis_client import redis_client await redis_client.disconnect() logger.info("redis_disconnected") except Exception as e: logger.error("redis_disconnect_failed", error=str(e)) # Close gRPC SDK Bridge connections try: from clients.sdk_bridge_client import sdk_bridge_client await sdk_bridge_client.disconnect() logger.info("sdk_bridge_disconnected") except Exception as e: logger.error("sdk_bridge_disconnect_failed", error=str(e)) # Close database connections try: from models import engine await engine.dispose() logger.info("database_disconnected") except Exception as e: logger.error("database_disconnect_failed", error=str(e)) logger.info("shutdown_complete") # Health check endpoint @app.get("/health", tags=["system"]) async def health_check(): """ Enhanced health check endpoint Checks connectivity to: - Database (PostgreSQL) - Redis cache - SDK Bridge (gRPC) Returns overall status and individual component statuses """ health_status = { "status": "healthy", "version": settings.API_VERSION, "environment": settings.ENVIRONMENT, "timestamp": datetime.utcnow().isoformat(), "components": {} } all_healthy = True # Check database connectivity try: from models import engine async with engine.connect() as conn: await conn.execute(sa.text("SELECT 1")) health_status["components"]["database"] = { "status": "healthy", "type": "postgresql" } except Exception as e: health_status["components"]["database"] = { "status": "unhealthy", "error": str(e) } all_healthy = False # Check Redis connectivity try: from clients.redis_client import redis_client await redis_client.ping() health_status["components"]["redis"] = { "status": "healthy", "type": "redis" } except Exception as e: health_status["components"]["redis"] = { "status": "unhealthy", "error": str(e) } all_healthy = False # Check SDK Bridge connectivity try: from clients.sdk_bridge_client import sdk_bridge_client # Attempt to call health check on SDK Bridge await sdk_bridge_client.health_check() health_status["components"]["sdk_bridge"] = { "status": "healthy", "type": "grpc" } except Exception as e: health_status["components"]["sdk_bridge"] = { "status": "unhealthy", "error": str(e) } all_healthy = False # Set overall status if not all_healthy: health_status["status"] = "degraded" return health_status # Metrics endpoint @app.get("/metrics", tags=["system"]) async def metrics(): """ Metrics endpoint Provides basic API metrics: - Total routes registered - API version - Environment """ return { "api_version": settings.API_VERSION, "environment": settings.ENVIRONMENT, "routes": { "total": len(app.routes), "auth": 4, # login, logout, refresh, me "cameras": 6, # list, detail, refresh, search, online, ptz "monitors": 7, # list, detail, refresh, search, available, active, routing "crossswitch": 4 # execute, clear, routing, history }, "features": { "authentication": True, "camera_discovery": True, "monitor_discovery": True, "cross_switching": True, "audit_logging": True, "redis_caching": True } } # Root endpoint @app.get("/", tags=["system"]) async def root(): """API root endpoint""" return { "name": settings.API_TITLE, "version": settings.API_VERSION, "docs": "/docs", "health": "/health", "metrics": "/metrics" } # Register routers from routers import auth, cameras, monitors, crossswitch app.include_router(auth.router) app.include_router(cameras.router) app.include_router(monitors.router) app.include_router(crossswitch.router) if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host=settings.API_HOST, port=settings.API_PORT, reload=settings.ENVIRONMENT == "development" )