Files
sac4cps-backend/main_layered.py
rafaeldpsilva a7a18e6295 first commit
2025-09-09 13:46:42 +01:00

273 lines
9.2 KiB
Python

"""
Main application entry point with layered architecture
This is the new structured version of the FastAPI application
"""
import asyncio
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import logging
# Import layered components
from layers.infrastructure.database_connection import database_connection
from layers.infrastructure.redis_connection import redis_connection
from layers.business.sensor_service import SensorService
from layers.business.cleanup_service import cleanup_service
from layers.presentation.websocket_handler import websocket_manager
from layers.presentation.redis_subscriber import redis_subscriber
from layers.presentation.api_routes import router as api_router
from models import HealthCheck
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Application startup time for uptime calculation
app_start_time = time.time()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager with proper layer initialization"""
# Startup
logger.info("Application starting up...")
try:
# Initialize infrastructure layer
await database_connection.connect()
await redis_connection.connect()
logger.info("Infrastructure layer initialized")
# Initialize business layer
sensor_service = SensorService() # Services are initialized on-demand
logger.info("Business layer initialized")
# Initialize presentation layer
await redis_subscriber.start_subscription("energy_data")
await cleanup_service.start_scheduled_cleanup(24) # Daily cleanup
logger.info("Presentation layer initialized")
logger.info("Application startup complete")
yield
# Shutdown
logger.info("Application shutting down...")
# Stop background tasks
await redis_subscriber.stop_subscription()
await cleanup_service.stop_scheduled_cleanup()
# Close connections
await database_connection.disconnect()
await redis_connection.disconnect()
logger.info("Application shutdown complete")
except Exception as e:
logger.error(f"Error during application lifecycle: {e}")
raise
app = FastAPI(
title="Energy Monitoring Dashboard API",
description="Real-time energy monitoring and IoT sensor data management system (Layered Architecture)",
version="2.0.0",
lifespan=lifespan
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure appropriately for production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router with version prefix
app.include_router(api_router, prefix="/api/v1")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket endpoint for real-time data streaming
Presentation Layer - handles WebSocket connections
"""
await websocket_manager.connect(websocket)
try:
while True:
# Keep the connection alive by waiting for messages
await websocket.receive_text()
except WebSocketDisconnect:
websocket_manager.disconnect(websocket)
@app.get("/")
async def read_root():
"""Root endpoint with basic system information"""
return {
"message": "Energy Monitoring Dashboard Backend (Layered Architecture)",
"version": "2.0.0",
"status": "running",
"uptime_seconds": time.time() - app_start_time,
"architecture": "3-layer (Presentation, Business, Infrastructure)"
}
@app.get("/health", response_model=HealthCheck)
async def health_check():
"""
Comprehensive health check endpoint
Checks all layers and dependencies
"""
try:
# Check infrastructure layer
mongodb_connected = True
redis_connected = True
try:
db = await database_connection.get_database()
await db.command("ping")
except:
mongodb_connected = False
try:
redis_client = await redis_connection.get_client()
await redis_client.ping()
except:
redis_connected = False
# Check business layer through service
sensor_service = SensorService()
from layers.infrastructure.repositories import SensorReadingRepository
stats_repo = SensorReadingRepository()
# Get basic statistics
try:
# Simple count queries to test business layer
total_readings = await stats_repo.count_by_query({})
active_sensors_data = await redis_connection.get_keys_by_pattern("sensor:latest:*")
total_sensors = len(active_sensors_data)
except Exception as e:
logger.error(f"Error getting stats for health check: {e}")
total_readings = 0
total_sensors = 0
# Check presentation layer
websocket_connections = websocket_manager.get_connection_count()
redis_subscription_active = redis_subscriber.is_subscriber_running()
# Determine overall status
status = "healthy"
if not mongodb_connected or not redis_connected:
status = "degraded"
if not mongodb_connected and not redis_connected:
status = "unhealthy"
return HealthCheck(
status=status,
mongodb_connected=mongodb_connected,
redis_connected=redis_connected,
total_sensors=total_sensors,
active_sensors=total_sensors, # Approximation
total_readings=total_readings,
uptime_seconds=time.time() - app_start_time
)
except Exception as e:
logger.error(f"Health check failed: {e}")
raise HTTPException(status_code=503, detail="Service Unavailable")
@app.get("/status")
async def system_status():
"""
Detailed system status endpoint with layer-specific information
"""
try:
# Infrastructure layer status
infrastructure_status = {
"database_connected": True,
"redis_connected": True
}
try:
db = await database_connection.get_database()
await db.command("ping")
except:
infrastructure_status["database_connected"] = False
try:
redis_client = await redis_connection.get_client()
await redis_client.ping()
except:
infrastructure_status["redis_connected"] = False
# Business layer status
business_status = {
"cleanup_service_running": cleanup_service.is_cleanup_running()
}
# Presentation layer status
presentation_status = {
"active_websocket_connections": websocket_manager.get_connection_count(),
"redis_subscriber_running": redis_subscriber.is_subscriber_running()
}
# Get subscriber status details
subscriber_status = await redis_subscriber.get_subscriber_status()
return {
"timestamp": time.time(),
"uptime_seconds": time.time() - app_start_time,
"architecture": "layered",
"layers": {
"infrastructure": infrastructure_status,
"business": business_status,
"presentation": presentation_status
},
"redis_subscriber": subscriber_status
}
except Exception as e:
logger.error(f"Status check failed: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
@app.get("/system/cleanup", summary="Get cleanup service status")
async def get_cleanup_status():
"""Get data cleanup service status and statistics"""
try:
# Get cleanup service status
cleanup_running = cleanup_service.is_cleanup_running()
# Get storage statistics
storage_stats = await cleanup_service.get_storage_statistics()
# Get retention policy info
retention_info = await cleanup_service.get_data_retention_info()
return {
"cleanup_service_running": cleanup_running,
"storage_statistics": storage_stats,
"retention_policies": retention_info
}
except Exception as e:
logger.error(f"Error getting cleanup status: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
@app.post("/system/cleanup", summary="Run manual cleanup")
async def run_manual_cleanup():
"""Manually trigger data cleanup process"""
try:
cleanup_results = await cleanup_service.cleanup_old_data()
return {
"message": "Manual cleanup completed",
"results": cleanup_results
}
except Exception as e:
logger.error(f"Error running manual cleanup: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)