|
| 1 | +"""FastAPI application factory for LG-SOTF API.""" |
| 2 | + |
| 3 | +import asyncio |
| 4 | +import logging |
| 5 | +from typing import Optional |
| 6 | + |
| 7 | +from fastapi import FastAPI |
| 8 | +from fastapi.middleware.cors import CORSMiddleware |
| 9 | + |
| 10 | +from lg_sotf.app_initializer import LG_SOTFApplication |
| 11 | +from lg_sotf.api.utils.websocket import WebSocketManager |
| 12 | +from lg_sotf.api.routers import ( |
| 13 | + alerts, |
| 14 | + correlations, |
| 15 | + dashboard, |
| 16 | + escalations, |
| 17 | + ingestion, |
| 18 | + metrics, |
| 19 | + websocket, |
| 20 | +) |
| 21 | + |
| 22 | +logger = logging.getLogger(__name__) |
| 23 | + |
| 24 | + |
| 25 | +def create_app( |
| 26 | + config_path: str = "configs/development.yaml", |
| 27 | + setup_signal_handlers: bool = False, |
| 28 | +) -> FastAPI: |
| 29 | + """Create and configure the FastAPI application. |
| 30 | +
|
| 31 | + Args: |
| 32 | + config_path: Path to configuration file |
| 33 | + setup_signal_handlers: Whether to setup signal handlers (False for uvicorn) |
| 34 | +
|
| 35 | + Returns: |
| 36 | + Configured FastAPI application instance |
| 37 | + """ |
| 38 | + # Configure logging |
| 39 | + logging.basicConfig( |
| 40 | + level=logging.INFO, |
| 41 | + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
| 42 | + ) |
| 43 | + |
| 44 | + # Create FastAPI app |
| 45 | + app = FastAPI( |
| 46 | + title="LG-SOTF Dashboard API", |
| 47 | + description="Production-grade SOC Dashboard API", |
| 48 | + version="1.0.0", |
| 49 | + docs_url="/api/docs", |
| 50 | + redoc_url="/api/redoc" |
| 51 | + ) |
| 52 | + |
| 53 | + # Add CORS middleware |
| 54 | + app.add_middleware( |
| 55 | + CORSMiddleware, |
| 56 | + allow_origins=["*"], |
| 57 | + allow_credentials=True, |
| 58 | + allow_methods=["*"], |
| 59 | + allow_headers=["*"], |
| 60 | + ) |
| 61 | + |
| 62 | + # Create application instances (will be initialized in startup event) |
| 63 | + lg_sotf_app = LG_SOTFApplication( |
| 64 | + config_path=config_path, |
| 65 | + setup_signal_handlers=setup_signal_handlers |
| 66 | + ) |
| 67 | + ws_manager = WebSocketManager() |
| 68 | + |
| 69 | + # Store in app state for dependency injection |
| 70 | + app.state.lg_sotf_app = lg_sotf_app |
| 71 | + app.state.ws_manager = ws_manager |
| 72 | + |
| 73 | + # Track background tasks for proper shutdown |
| 74 | + app.state.background_tasks = [] |
| 75 | + |
| 76 | + # Register routers |
| 77 | + app.include_router(metrics.router) |
| 78 | + app.include_router(alerts.router) |
| 79 | + app.include_router(ingestion.router) |
| 80 | + app.include_router(dashboard.router) |
| 81 | + app.include_router(correlations.router) |
| 82 | + app.include_router(escalations.router) |
| 83 | + app.include_router(websocket.router) |
| 84 | + |
| 85 | + # Startup event handler |
| 86 | + @app.on_event("startup") |
| 87 | + async def startup(): |
| 88 | + """Initialize application on startup.""" |
| 89 | + logger.info("Starting LG-SOTF API server...") |
| 90 | + |
| 91 | + # Initialize LG-SOTF application |
| 92 | + await lg_sotf_app.initialize() |
| 93 | + logger.info("LG-SOTF application initialized") |
| 94 | + |
| 95 | + # Start background tasks |
| 96 | + _start_background_tasks(app) |
| 97 | + logger.info("Background tasks started") |
| 98 | + |
| 99 | + logger.info("✅ LG-SOTF API server ready") |
| 100 | + |
| 101 | + # Shutdown event handler |
| 102 | + @app.on_event("shutdown") |
| 103 | + async def shutdown(): |
| 104 | + """Cleanup on shutdown.""" |
| 105 | + logger.info("🛑 Shutting down API server...") |
| 106 | + |
| 107 | + # Close all WebSocket connections |
| 108 | + if ws_manager.active_connections: |
| 109 | + logger.info(f"Closing {len(ws_manager.active_connections)} WebSocket connections...") |
| 110 | + connections = list(ws_manager.active_connections.values()) |
| 111 | + for ws in connections: |
| 112 | + try: |
| 113 | + await ws.close() |
| 114 | + except Exception as e: |
| 115 | + logger.error(f"Error closing WebSocket: {e}") |
| 116 | + ws_manager.active_connections.clear() |
| 117 | + logger.info("✓ WebSocket connections closed") |
| 118 | + |
| 119 | + # Cancel background tasks |
| 120 | + if app.state.background_tasks: |
| 121 | + logger.info(f"Cancelling {len(app.state.background_tasks)} background tasks...") |
| 122 | + for task in app.state.background_tasks: |
| 123 | + if not task.done(): |
| 124 | + task.cancel() |
| 125 | + |
| 126 | + # Wait for tasks to complete with timeout |
| 127 | + try: |
| 128 | + await asyncio.wait_for( |
| 129 | + asyncio.gather(*app.state.background_tasks, return_exceptions=True), |
| 130 | + timeout=5.0 |
| 131 | + ) |
| 132 | + logger.info("✓ Background tasks cancelled") |
| 133 | + except asyncio.TimeoutError: |
| 134 | + logger.warning("⚠ Some background tasks did not complete within timeout") |
| 135 | + |
| 136 | + # Shutdown LG-SOTF application |
| 137 | + await lg_sotf_app.shutdown() |
| 138 | + logger.info("✅ Shutdown complete") |
| 139 | + |
| 140 | + return app |
| 141 | + |
| 142 | + |
| 143 | +def _start_background_tasks(app: FastAPI): |
| 144 | + """Start background monitoring and update tasks.""" |
| 145 | + ws_manager: WebSocketManager = app.state.ws_manager |
| 146 | + lg_sotf_app: LG_SOTFApplication = app.state.lg_sotf_app |
| 147 | + |
| 148 | + async def metrics_updater(): |
| 149 | + """Periodically collect and broadcast system metrics.""" |
| 150 | + while True: |
| 151 | + try: |
| 152 | + await asyncio.sleep(10) |
| 153 | + |
| 154 | + # Import here to avoid circular dependency |
| 155 | + from lg_sotf.api.routers.metrics import _collect_system_metrics |
| 156 | + |
| 157 | + metrics = await _collect_system_metrics(lg_sotf_app) |
| 158 | + |
| 159 | + await ws_manager.broadcast({ |
| 160 | + "type": "system_metrics", |
| 161 | + "data": metrics.model_dump() |
| 162 | + }, "system_metrics") |
| 163 | + |
| 164 | + except asyncio.CancelledError: |
| 165 | + logger.info("Metrics updater cancelled") |
| 166 | + break |
| 167 | + except Exception as e: |
| 168 | + logger.error(f"Metrics updater error: {e}") |
| 169 | + |
| 170 | + async def ingestion_monitor(): |
| 171 | + """Monitor ingestion activity and broadcast updates.""" |
| 172 | + while True: |
| 173 | + try: |
| 174 | + await asyncio.sleep(5) # Check every 5 seconds |
| 175 | + |
| 176 | + if not lg_sotf_app.workflow_engine: |
| 177 | + continue |
| 178 | + |
| 179 | + ingestion_agent = ( |
| 180 | + lg_sotf_app.workflow_engine.agents.get("ingestion_instance") or |
| 181 | + lg_sotf_app.workflow_engine.agents.get("ingestion") |
| 182 | + ) |
| 183 | + |
| 184 | + if not ingestion_agent: |
| 185 | + continue |
| 186 | + |
| 187 | + # Broadcast ingestion stats |
| 188 | + await ws_manager.broadcast({ |
| 189 | + "type": "ingestion_stats", |
| 190 | + "data": { |
| 191 | + "total_ingested": ingestion_agent.ingestion_stats["total_ingested"], |
| 192 | + "total_deduplicated": ingestion_agent.ingestion_stats["total_deduplicated"], |
| 193 | + "total_errors": ingestion_agent.ingestion_stats["total_errors"], |
| 194 | + "by_source": dict(ingestion_agent.ingestion_stats["by_source"]), |
| 195 | + "enabled_sources": ingestion_agent.enabled_sources, |
| 196 | + "last_poll": lg_sotf_app._last_ingestion_poll.isoformat() if lg_sotf_app._last_ingestion_poll else None |
| 197 | + } |
| 198 | + }, "ingestion_updates") |
| 199 | + |
| 200 | + except asyncio.CancelledError: |
| 201 | + logger.info("Ingestion monitor cancelled") |
| 202 | + break |
| 203 | + except Exception as e: |
| 204 | + logger.error(f"Ingestion monitor error: {e}") |
| 205 | + |
| 206 | + # Create and track background tasks |
| 207 | + app.state.background_tasks.append(asyncio.create_task(ws_manager.heartbeat_loop())) |
| 208 | + app.state.background_tasks.append(asyncio.create_task(metrics_updater())) |
| 209 | + app.state.background_tasks.append(asyncio.create_task(ingestion_monitor())) |
| 210 | + |
| 211 | + |
| 212 | +# Create app instance for uvicorn |
| 213 | +app = create_app() |
0 commit comments