2025-07-21 22:44:11 +02:00
|
|
|
# ================================================================
|
2025-08-23 10:19:58 +02:00
|
|
|
# services/notification/app/main.py - ENHANCED WITH SSE SUPPORT
|
2025-07-21 22:44:11 +02:00
|
|
|
# ================================================================
|
2025-07-17 13:09:24 +02:00
|
|
|
"""
|
2025-07-21 22:44:11 +02:00
|
|
|
Notification Service Main Application
|
2025-08-23 10:19:58 +02:00
|
|
|
Handles email, WhatsApp notifications and SSE for real-time alerts/recommendations
|
2025-07-17 13:09:24 +02:00
|
|
|
"""
|
|
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
from fastapi import FastAPI
|
2025-09-30 08:12:45 +02:00
|
|
|
from sqlalchemy import text
|
2025-07-17 13:09:24 +02:00
|
|
|
from app.core.config import settings
|
2025-09-29 13:13:12 +02:00
|
|
|
from app.core.database import database_manager
|
2025-07-21 22:44:11 +02:00
|
|
|
from app.api.notifications import router as notification_router
|
2025-10-06 15:27:01 +02:00
|
|
|
from app.api.notification_operations import router as notification_operations_router
|
|
|
|
|
from app.api.analytics import router as analytics_router
|
2025-11-02 20:24:44 +01:00
|
|
|
from app.api.audit import router as audit_router
|
2025-07-21 22:44:11 +02:00
|
|
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
2025-08-23 10:19:58 +02:00
|
|
|
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
|
2025-09-29 13:13:12 +02:00
|
|
|
from shared.service_base import StandardFastAPIService
|
2025-07-17 13:09:24 +02:00
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
|
|
|
|
|
class NotificationService(StandardFastAPIService):
|
|
|
|
|
"""Notification Service with standardized setup"""
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
expected_migration_version = "359991e24ea2"
|
2025-09-30 08:12:45 +02:00
|
|
|
|
|
|
|
|
async def verify_migrations(self):
|
|
|
|
|
"""Verify database schema matches the latest migrations."""
|
|
|
|
|
try:
|
|
|
|
|
async with self.database_manager.get_session() as session:
|
|
|
|
|
result = await session.execute(text("SELECT version_num FROM alembic_version"))
|
|
|
|
|
version = result.scalar()
|
|
|
|
|
if version != self.expected_migration_version:
|
|
|
|
|
self.logger.error(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}")
|
|
|
|
|
raise RuntimeError(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}")
|
|
|
|
|
self.logger.info(f"Migration verification successful: {version}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"Migration verification failed: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
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
|
2025-10-01 14:39:10 +02:00
|
|
|
#async def check_email_service():
|
|
|
|
|
# """Check email service health - service is ready even if credentials are invalid"""
|
|
|
|
|
# try:
|
|
|
|
|
# if not self.email_service:
|
|
|
|
|
# return False
|
|
|
|
|
# # Service is considered healthy if it's initialized, even if credentials fail
|
|
|
|
|
# # This allows the pod to be ready while external services may have config issues
|
|
|
|
|
# await self.email_service.health_check()
|
|
|
|
|
# return True
|
|
|
|
|
# except Exception as e:
|
2025-10-01 12:28:00 +02:00
|
|
|
# Log but don't fail readiness - email service config issues shouldn't block the pod
|
2025-10-01 14:39:10 +02:00
|
|
|
# self.logger.error("Email service health check failed", error=str(e))
|
2025-10-01 12:28:00 +02:00
|
|
|
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
2025-10-01 14:39:10 +02:00
|
|
|
# return True
|
2025-09-29 13:13:12 +02:00
|
|
|
|
2025-10-01 14:39:10 +02:00
|
|
|
#async def check_whatsapp_service():
|
|
|
|
|
# """Check WhatsApp service health - service is ready even if credentials are invalid"""
|
|
|
|
|
# try:
|
|
|
|
|
# if not self.whatsapp_service:
|
|
|
|
|
# return False
|
2025-10-01 12:28:00 +02:00
|
|
|
# Service is considered healthy if it's initialized, even if credentials fail
|
2025-10-01 14:39:10 +02:00
|
|
|
# await self.whatsapp_service.health_check()
|
|
|
|
|
# return True
|
|
|
|
|
# except Exception as e:
|
2025-10-01 12:28:00 +02:00
|
|
|
# Log but don't fail readiness - WhatsApp config issues shouldn't block the pod
|
2025-10-01 14:39:10 +02:00
|
|
|
# self.logger.error("WhatsApp service health check failed", error=str(e))
|
2025-10-01 12:28:00 +02:00
|
|
|
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
2025-10-01 14:39:10 +02:00
|
|
|
# return True
|
2025-09-29 13:13:12 +02:00
|
|
|
|
2025-08-23 10:19:58 +02:00
|
|
|
async def check_sse_service():
|
2025-09-29 13:13:12 +02:00
|
|
|
"""Check SSE service health"""
|
2025-08-23 10:19:58 +02:00
|
|
|
try:
|
2025-09-29 13:13:12 +02:00
|
|
|
if self.sse_service:
|
|
|
|
|
metrics = self.sse_service.get_metrics()
|
|
|
|
|
return bool(metrics.get("redis_connected", False))
|
|
|
|
|
return False
|
2025-08-23 10:19:58 +02:00
|
|
|
except Exception as e:
|
2025-09-29 13:13:12 +02:00
|
|
|
self.logger.error("SSE service health check failed", error=str(e))
|
|
|
|
|
return False
|
|
|
|
|
|
2025-10-01 14:39:10 +02:00
|
|
|
#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
|
2025-09-29 13:13:12 +02:00
|
|
|
|
|
|
|
|
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', ["*"]),
|
2025-10-06 15:27:01 +02:00
|
|
|
api_prefix="", # Empty because RouteBuilder already includes /api/v1
|
2025-09-29 13:13:12 +02:00
|
|
|
database_manager=database_manager,
|
|
|
|
|
expected_tables=notification_expected_tables,
|
|
|
|
|
custom_health_checks={
|
2025-10-01 14:39:10 +02:00
|
|
|
# "email_service": check_email_service,
|
|
|
|
|
# "whatsapp_service": check_whatsapp_service,
|
2025-09-29 13:13:12 +02:00
|
|
|
"sse_service": check_sse_service,
|
2025-10-01 14:39:10 +02:00
|
|
|
# "messaging": check_messaging
|
2025-09-29 13:13:12 +02:00
|
|
|
},
|
|
|
|
|
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"""
|
2025-07-21 22:44:11 +02:00
|
|
|
await cleanup_messaging()
|
2025-09-29 13:13:12 +02:00
|
|
|
|
|
|
|
|
async def on_startup(self, app: FastAPI):
|
|
|
|
|
"""Custom startup logic for notification service"""
|
2025-10-15 16:12:49 +02:00
|
|
|
# Verify migrations first
|
|
|
|
|
await self.verify_migrations()
|
|
|
|
|
|
|
|
|
|
# Call parent startup (includes database, messaging, etc.)
|
|
|
|
|
await super().on_startup(app)
|
|
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
# Initialize services
|
|
|
|
|
self.email_service = EmailService()
|
|
|
|
|
self.whatsapp_service = WhatsAppService()
|
|
|
|
|
|
|
|
|
|
# Initialize SSE service
|
2025-10-15 16:12:49 +02:00
|
|
|
self.sse_service = SSEService()
|
|
|
|
|
await self.sse_service.initialize(settings.REDIS_URL)
|
2025-09-29 13:13:12 +02:00
|
|
|
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"
|
2025-07-17 13:09:24 +02:00
|
|
|
)
|
|
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
# Setup standard endpoints
|
|
|
|
|
service.setup_standard_endpoints()
|
|
|
|
|
|
|
|
|
|
# Setup custom endpoints
|
|
|
|
|
service.setup_custom_endpoints()
|
2025-07-21 22:44:11 +02:00
|
|
|
|
2025-09-29 13:13:12 +02:00
|
|
|
# Include routers
|
2025-11-02 20:24:44 +01:00
|
|
|
# IMPORTANT: Register audit router FIRST to avoid route matching conflicts
|
|
|
|
|
# where {notification_id} would match literal paths like "audit-logs"
|
|
|
|
|
service.add_router(audit_router, tags=["audit-logs"])
|
2025-10-06 15:27:01 +02:00
|
|
|
service.add_router(notification_operations_router, tags=["notification-operations"])
|
|
|
|
|
service.add_router(analytics_router, tags=["notifications-analytics"])
|
2025-11-02 20:24:44 +01:00
|
|
|
service.add_router(notification_router, tags=["notifications"])
|
2025-07-17 13:09:24 +02:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import uvicorn
|
2025-10-15 16:12:49 +02:00
|
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|