Refactor all main.py

This commit is contained in:
Urtzi Alfaro
2025-09-29 13:13:12 +02:00
parent 4777e59e7a
commit befcc126b0
35 changed files with 2537 additions and 1993 deletions

View File

@@ -6,14 +6,9 @@ Notification Service Main Application
Handles email, WhatsApp notifications and SSE for real-time alerts/recommendations
"""
import structlog
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi import FastAPI
from app.core.config import settings
from app.core.database import init_db
from app.core.database import database_manager
from app.api.notifications import router as notification_router
from app.api.sse_routes import router as sse_router
from app.services.messaging import setup_messaging, cleanup_messaging
@@ -21,226 +16,207 @@ from app.services.sse_service import SSEService
from app.services.notification_orchestrator import NotificationOrchestrator
from app.services.email_service import EmailService
from app.services.whatsapp_service import WhatsAppService
from shared.monitoring import setup_logging, HealthChecker
from shared.monitoring.metrics import setup_metrics_early
from shared.service_base import StandardFastAPIService
# Setup logging first
setup_logging("notification-service", settings.LOG_LEVEL)
logger = structlog.get_logger()
# Global variables for lifespan access
metrics_collector = None
health_checker = None
class NotificationService(StandardFastAPIService):
"""Notification Service with standardized setup"""
# Create FastAPI app FIRST
app = FastAPI(
title="Bakery Notification Service",
description="Email, WhatsApp and SSE notification service for bakery alerts and recommendations",
version="2.0.0",
def __init__(self):
# Define expected database tables for health checks
notification_expected_tables = [
'notifications', 'notification_templates', 'notification_preferences',
'notification_logs', 'email_templates', 'whatsapp_templates'
]
self.sse_service = None
self.orchestrator = None
self.email_service = None
self.whatsapp_service = None
# Define custom metrics for notification service
notification_custom_metrics = {
"notifications_sent_total": {
"type": "counter",
"description": "Total notifications sent",
"labels": ["type", "status", "channel"]
},
"emails_sent_total": {
"type": "counter",
"description": "Total emails sent",
"labels": ["status"]
},
"whatsapp_sent_total": {
"type": "counter",
"description": "Total WhatsApp messages sent",
"labels": ["status"]
},
"sse_events_sent_total": {
"type": "counter",
"description": "Total SSE events sent",
"labels": ["tenant", "event_type"]
},
"notification_processing_duration_seconds": {
"type": "histogram",
"description": "Time spent processing notifications"
}
}
# Define custom health checks for notification service components
async def check_email_service():
"""Check email service health"""
try:
return await self.email_service.health_check() if self.email_service else False
except Exception as e:
self.logger.error("Email service health check failed", error=str(e))
return False
async def check_whatsapp_service():
"""Check WhatsApp service health"""
try:
return await self.whatsapp_service.health_check() if self.whatsapp_service else False
except Exception as e:
self.logger.error("WhatsApp service health check failed", error=str(e))
return False
async def check_sse_service():
"""Check SSE service health"""
try:
if self.sse_service:
metrics = self.sse_service.get_metrics()
return bool(metrics.get("redis_connected", False))
return False
except Exception as e:
self.logger.error("SSE service health check failed", error=str(e))
return False
async def check_messaging():
"""Check messaging service health"""
try:
from app.services.messaging import notification_publisher
return bool(notification_publisher and notification_publisher.connected)
except Exception as e:
self.logger.error("Messaging health check failed", error=str(e))
return False
super().__init__(
service_name="notification-service",
app_name="Bakery Notification Service",
description="Email, WhatsApp and SSE notification service for bakery alerts and recommendations",
version="2.0.0",
log_level=settings.LOG_LEVEL,
cors_origins=getattr(settings, 'CORS_ORIGINS', ["*"]),
api_prefix="/api/v1",
database_manager=database_manager,
expected_tables=notification_expected_tables,
custom_health_checks={
"email_service": check_email_service,
"whatsapp_service": check_whatsapp_service,
"sse_service": check_sse_service,
"messaging": check_messaging
},
enable_messaging=True,
custom_metrics=notification_custom_metrics
)
async def _setup_messaging(self):
"""Setup messaging for notification service"""
await setup_messaging()
self.logger.info("Messaging initialized")
async def _cleanup_messaging(self):
"""Cleanup messaging for notification service"""
await cleanup_messaging()
async def on_startup(self, app: FastAPI):
"""Custom startup logic for notification service"""
# Initialize services
self.email_service = EmailService()
self.whatsapp_service = WhatsAppService()
# Initialize SSE service
self.sse_service = SSEService(settings.REDIS_URL)
await self.sse_service.initialize()
self.logger.info("SSE service initialized")
# Create orchestrator
self.orchestrator = NotificationOrchestrator(
email_service=self.email_service,
whatsapp_service=self.whatsapp_service,
sse_service=self.sse_service
)
# Store services in app state
app.state.orchestrator = self.orchestrator
app.state.sse_service = self.sse_service
app.state.email_service = self.email_service
app.state.whatsapp_service = self.whatsapp_service
async def on_shutdown(self, app: FastAPI):
"""Custom shutdown logic for notification service"""
# Shutdown SSE service
if self.sse_service:
await self.sse_service.shutdown()
self.logger.info("SSE service shutdown completed")
def get_service_features(self):
"""Return notification-specific features"""
return [
"email_notifications",
"whatsapp_notifications",
"sse_real_time_updates",
"notification_templates",
"notification_orchestration",
"messaging_integration",
"multi_channel_support"
]
def setup_custom_endpoints(self):
"""Setup custom endpoints for notification service"""
# SSE metrics endpoint
@self.app.get("/sse-metrics")
async def sse_metrics():
"""Get SSE service metrics"""
if self.sse_service:
try:
sse_metrics = self.sse_service.get_metrics()
return {
'active_tenants': sse_metrics.get('active_tenants', 0),
'total_connections': sse_metrics.get('total_connections', 0),
'active_listeners': sse_metrics.get('active_listeners', 0),
'redis_connected': bool(sse_metrics.get('redis_connected', False))
}
except Exception as e:
return {"error": str(e)}
return {"error": "SSE service not available"}
# Metrics endpoint
@self.app.get("/metrics")
async def metrics():
"""Prometheus metrics endpoint"""
if self.metrics_collector:
return self.metrics_collector.get_metrics()
return {"metrics": "not_available"}
# Create service instance
service = NotificationService()
# Create FastAPI app with standardized setup
app = service.create_app(
docs_url="/docs",
redoc_url="/redoc"
)
# Setup metrics BEFORE any middleware and BEFORE lifespan
metrics_collector = setup_metrics_early(app, "notification-service")
# Setup standard endpoints
service.setup_standard_endpoints()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan events - NO MIDDLEWARE ADDED HERE"""
global health_checker
# Startup
logger.info("Starting Notification Service...")
try:
# Initialize database
await init_db()
logger.info("Database initialized")
# Setup messaging
await setup_messaging()
logger.info("Messaging initialized")
# Initialize services
email_service = EmailService()
whatsapp_service = WhatsAppService()
# Initialize SSE service
sse_service = SSEService(settings.REDIS_URL)
await sse_service.initialize()
logger.info("SSE service initialized")
# Create orchestrator
orchestrator = NotificationOrchestrator(
email_service=email_service,
whatsapp_service=whatsapp_service,
sse_service=sse_service
)
# Store services in app state
app.state.orchestrator = orchestrator
app.state.sse_service = sse_service
app.state.email_service = email_service
app.state.whatsapp_service = whatsapp_service
# Register custom metrics (metrics_collector already exists)
metrics_collector.register_counter("notifications_sent_total", "Total notifications sent", labels=["type", "status", "channel"])
metrics_collector.register_counter("emails_sent_total", "Total emails sent", labels=["status"])
metrics_collector.register_counter("whatsapp_sent_total", "Total WhatsApp messages sent", labels=["status"])
metrics_collector.register_counter("sse_events_sent_total", "Total SSE events sent", labels=["tenant", "event_type"])
metrics_collector.register_histogram("notification_processing_duration_seconds", "Time spent processing notifications")
metrics_collector.register_gauge("notification_queue_size", "Current notification queue size")
metrics_collector.register_gauge("sse_active_connections", "Number of active SSE connections")
# Setup health checker
health_checker = HealthChecker("notification-service")
# Add database health check
async def check_database():
try:
from app.core.database import get_db
from sqlalchemy import text
async for db in get_db():
await db.execute(text("SELECT 1"))
return True
except Exception as e:
return f"Database error: {e}"
health_checker.add_check("database", check_database, timeout=5.0, critical=True)
# Add email service health check
async def check_email_service():
try:
from app.services.email_service import EmailService
email_service = EmailService()
return await email_service.health_check()
except Exception as e:
return f"Email service error: {e}"
health_checker.add_check("email_service", check_email_service, timeout=10.0, critical=True)
# Add WhatsApp service health check
async def check_whatsapp_service():
try:
return await whatsapp_service.health_check()
except Exception as e:
return f"WhatsApp service error: {e}"
health_checker.add_check("whatsapp_service", check_whatsapp_service, timeout=10.0, critical=False)
# Add SSE service health check
async def check_sse_service():
try:
metrics = sse_service.get_metrics()
return "healthy" if metrics["redis_connected"] else "Redis connection failed"
except Exception as e:
return f"SSE service error: {e}"
health_checker.add_check("sse_service", check_sse_service, timeout=5.0, critical=True)
# Add messaging health check
def check_messaging():
try:
# Check if messaging is properly initialized
from app.services.messaging import notification_publisher
return notification_publisher.connected if notification_publisher else False
except Exception as e:
return f"Messaging error: {e}"
health_checker.add_check("messaging", check_messaging, timeout=3.0, critical=False)
# Store health checker in app state
app.state.health_checker = health_checker
logger.info("Notification Service with SSE support started successfully")
except Exception as e:
logger.error(f"Failed to start Notification Service: {e}")
raise
yield
# Shutdown
logger.info("Shutting down Notification Service...")
try:
# Shutdown SSE service
if hasattr(app.state, 'sse_service'):
await app.state.sse_service.shutdown()
logger.info("SSE service shutdown completed")
await cleanup_messaging()
logger.info("Messaging cleanup completed")
except Exception as e:
logger.error(f"Error during shutdown: {e}")
# Set lifespan AFTER metrics setup
app.router.lifespan_context = lifespan
# CORS middleware (added after metrics setup)
app.add_middleware(
CORSMiddleware,
allow_origins=getattr(settings, 'CORS_ORIGINS', ["*"]),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Setup custom endpoints
service.setup_custom_endpoints()
# Include routers
app.include_router(notification_router, prefix="/api/v1", tags=["notifications"])
app.include_router(sse_router, prefix="/api/v1", tags=["sse"])
# Health check endpoint
@app.get("/health")
async def health_check():
"""Comprehensive health check endpoint including SSE"""
if health_checker:
health_result = await health_checker.check_health()
# Add SSE metrics to health check
if hasattr(app.state, 'sse_service'):
try:
sse_metrics = app.state.sse_service.get_metrics()
# Convert metrics to JSON-serializable format
health_result['sse_metrics'] = {
'active_tenants': sse_metrics.get('active_tenants', 0),
'total_connections': sse_metrics.get('total_connections', 0),
'active_listeners': sse_metrics.get('active_listeners', 0),
'redis_connected': bool(sse_metrics.get('redis_connected', False))
}
except Exception as e:
health_result['sse_error'] = str(e)
return health_result
else:
return {
"service": "notification-service",
"status": "healthy",
"version": "2.0.0",
"features": ["email", "whatsapp", "sse", "alerts", "recommendations"]
}
# Metrics endpoint
@app.get("/metrics")
async def metrics():
"""Prometheus metrics endpoint"""
if metrics_collector:
return metrics_collector.get_metrics()
return {"metrics": "not_available"}
# Exception handlers
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Global exception handler with metrics"""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
# Record error metric if available
if metrics_collector:
metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
service.add_router(notification_router, tags=["notifications"])
service.add_router(sse_router, tags=["sse"])
if __name__ == "__main__":
import uvicorn