Refactor all main.py
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user