# ================================================================ # services/notification/app/main.py - ENHANCED WITH SSE SUPPORT # ================================================================ """ 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 app.core.config import settings from app.core.database import init_db 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 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 # 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 # 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", docs_url="/docs", redoc_url="/redoc" ) # Setup metrics BEFORE any middleware and BEFORE lifespan metrics_collector = setup_metrics_early(app, "notification-service") @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=["*"], ) # 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"} ) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)