273 lines
9.2 KiB
Python
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) |