diff --git a/services/orchestrator/app/api/dashboard.py b/services/orchestrator/app/api/dashboard.py index 4bf7bdb6..4fef550a 100644 --- a/services/orchestrator/app/api/dashboard.py +++ b/services/orchestrator/app/api/dashboard.py @@ -18,8 +18,10 @@ from ..services.dashboard_service import DashboardService from shared.clients import ( get_inventory_client, get_production_client, + get_alerts_client, ProductionServiceClient, - InventoryServiceClient + InventoryServiceClient, + AlertsServiceClient ) from shared.clients.procurement_client import ProcurementServiceClient @@ -29,6 +31,7 @@ logger = logging.getLogger(__name__) inventory_client = get_inventory_client(settings, "orchestrator") production_client = get_production_client(settings, "orchestrator") procurement_client = ProcurementServiceClient(settings) +alerts_client = get_alerts_client(settings, "orchestrator") router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/dashboard", tags=["dashboard"]) @@ -191,7 +194,7 @@ async def get_bakery_health_status( # Get alerts summary try: - alerts_data = await procurement_client.get_alerts_summary(tenant_id) or {} + alerts_data = await alerts_client.get_alerts_summary(tenant_id) or {} critical_alerts = alerts_data.get("critical_count", 0) except Exception as e: logger.warning(f"Failed to fetch alerts: {e}") @@ -331,7 +334,7 @@ async def get_action_queue( # Get critical alerts critical_alerts = [] try: - alerts_data = await procurement_client.get_critical_alerts(tenant_id, limit=20) + alerts_data = await alerts_client.get_critical_alerts(tenant_id, limit=20) if alerts_data: critical_alerts = alerts_data.get("alerts", []) except Exception as e: diff --git a/shared/clients/__init__.py b/shared/clients/__init__.py index 441657ab..9495a44c 100644 --- a/shared/clients/__init__.py +++ b/shared/clients/__init__.py @@ -17,6 +17,7 @@ from .recipes_client import RecipesServiceClient from .suppliers_client import SuppliersServiceClient from .tenant_client import TenantServiceClient from .ai_insights_client import AIInsightsClient +from .alerts_client import AlertsServiceClient # Import config from shared.config.base import BaseServiceSettings @@ -108,12 +109,22 @@ def get_suppliers_client(config: BaseServiceSettings = None, service_name: str = """Get or create a suppliers service client""" if config is None: from app.core.config import settings as config - + cache_key = f"suppliers_{service_name}" if cache_key not in _client_cache: _client_cache[cache_key] = SuppliersServiceClient(config) return _client_cache[cache_key] +def get_alerts_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> AlertsServiceClient: + """Get or create an alerts service client""" + if config is None: + from app.core.config import settings as config + + cache_key = f"alerts_{service_name}" + if cache_key not in _client_cache: + _client_cache[cache_key] = AlertsServiceClient(config, service_name) + return _client_cache[cache_key] + class ServiceClients: """Convenient wrapper for all service clients""" @@ -223,6 +234,7 @@ __all__ = [ 'ProductionServiceClient', 'RecipesServiceClient', 'SuppliersServiceClient', + 'AlertsServiceClient', 'TenantServiceClient', 'ServiceClients', 'get_training_client', @@ -234,5 +246,6 @@ __all__ = [ 'get_production_client', 'get_recipes_client', 'get_suppliers_client', + 'get_alerts_client', 'get_service_clients' ] \ No newline at end of file diff --git a/shared/clients/alerts_client.py b/shared/clients/alerts_client.py new file mode 100644 index 00000000..c1703cc9 --- /dev/null +++ b/shared/clients/alerts_client.py @@ -0,0 +1,173 @@ +# shared/clients/alerts_client.py +""" +Alerts Service Client for Inter-Service Communication +Provides access to alert processor service from other services +""" + +import structlog +from typing import Dict, Any, Optional, List +from uuid import UUID +from shared.clients.base_service_client import BaseServiceClient +from shared.config.base import BaseServiceSettings + +logger = structlog.get_logger() + + +class AlertsServiceClient(BaseServiceClient): + """Client for communicating with the Alert Processor Service""" + + def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"): + super().__init__(calling_service_name, config) + + def get_service_base_path(self) -> str: + return "/api/v1" + + # ================================================================ + # DASHBOARD METHODS + # ================================================================ + + async def get_alerts_summary( + self, + tenant_id: str + ) -> Optional[Dict[str, Any]]: + """ + Get alerts summary for dashboard health status + + Args: + tenant_id: Tenant ID + + Returns: + Dict with counts by severity: + { + "total_count": int, + "active_count": int, + "critical_count": int, # Maps to "urgent" severity + "high_count": int, + "medium_count": int, + "low_count": int, + "resolved_count": int, + "acknowledged_count": int + } + """ + try: + # Gateway routes /tenants/{tenant_id}/alerts/... to alert_processor service + return await self.get( + "/alerts/summary", + tenant_id=tenant_id + ) + except Exception as e: + logger.error("Error fetching alerts summary", error=str(e), tenant_id=tenant_id) + return None + + async def get_critical_alerts( + self, + tenant_id: str, + limit: int = 20 + ) -> Optional[Dict[str, Any]]: + """ + Get critical/urgent alerts for dashboard + + Note: "critical" in dashboard context maps to "urgent" severity in alert_processor + + Args: + tenant_id: Tenant ID + limit: Maximum number of alerts to return + + Returns: + Dict with: + { + "alerts": [...], + "total": int, + "limit": int, + "offset": int + } + """ + try: + # Gateway routes /tenants/{tenant_id}/alerts/... to alert_processor service + # "critical" in dashboard = "urgent" severity in alert_processor + return await self.get( + "/alerts", + tenant_id=tenant_id, + params={"severity": "urgent", "resolved": False, "limit": limit} + ) + except Exception as e: + logger.error("Error fetching critical alerts", error=str(e), tenant_id=tenant_id) + return None + + async def get_alerts_by_severity( + self, + tenant_id: str, + severity: str, + limit: int = 100, + resolved: Optional[bool] = None + ) -> Optional[Dict[str, Any]]: + """ + Get alerts filtered by severity + + Args: + tenant_id: Tenant ID + severity: Severity level (low, medium, high, urgent) + limit: Maximum number of alerts + resolved: Filter by resolved status (None = all, True = resolved only, False = unresolved only) + + Returns: + Dict with alerts list and metadata + """ + try: + params = {"severity": severity, "limit": limit} + if resolved is not None: + params["resolved"] = resolved + + return await self.get( + "/alerts", + tenant_id=tenant_id, + params=params + ) + except Exception as e: + logger.error("Error fetching alerts by severity", + error=str(e), severity=severity, tenant_id=tenant_id) + return None + + async def get_alert_by_id( + self, + tenant_id: str, + alert_id: str + ) -> Optional[Dict[str, Any]]: + """ + Get a specific alert by ID + + Args: + tenant_id: Tenant ID + alert_id: Alert UUID + + Returns: + Dict with alert details + """ + try: + return await self.get( + f"/alerts/{alert_id}", + tenant_id=tenant_id + ) + except Exception as e: + logger.error("Error fetching alert", error=str(e), + alert_id=alert_id, tenant_id=tenant_id) + return None + + # ================================================================ + # UTILITY METHODS + # ================================================================ + + async def health_check(self) -> bool: + """Check if alerts service is healthy""" + try: + result = await self.get("../health") # Health endpoint is not tenant-scoped + return result is not None + except Exception as e: + logger.error("Alerts service health check failed", error=str(e)) + return False + + +# Factory function for dependency injection +def create_alerts_client(config: BaseServiceSettings, calling_service_name: str = "unknown") -> AlertsServiceClient: + """Create alerts service client instance""" + return AlertsServiceClient(config, calling_service_name) diff --git a/shared/clients/procurement_client.py b/shared/clients/procurement_client.py index 6a3b7b0b..40459ef0 100644 --- a/shared/clients/procurement_client.py +++ b/shared/clients/procurement_client.py @@ -602,55 +602,3 @@ class ProcurementServiceClient(BaseServiceClient): except Exception as e: logger.error("Error fetching pending purchase orders", error=str(e), tenant_id=tenant_id) return None - - async def get_critical_alerts( - self, - tenant_id: str, - limit: int = 20 - ) -> Optional[Dict[str, Any]]: - """ - Get critical alerts for dashboard - - Note: "critical" maps to "urgent" severity in alert_processor service - - Args: - tenant_id: Tenant ID - limit: Maximum number of alerts to return - - Returns: - Dict with {"alerts": [...], "total": n} - """ - try: - # Gateway routes /tenants/{tenant_id}/alerts/... to alert_processor service - # "critical" in dashboard = "urgent" severity in alert_processor - return await self.get( - "/alerts", - tenant_id=tenant_id, - params={"severity": "urgent", "resolved": False, "limit": limit} - ) - except Exception as e: - logger.error("Error fetching critical alerts", error=str(e), tenant_id=tenant_id) - return None - - async def get_alerts_summary( - self, - tenant_id: str - ) -> Optional[Dict[str, Any]]: - """ - Get alerts summary for dashboard health status - - Args: - tenant_id: Tenant ID - - Returns: - Dict with counts by severity (critical_count maps to urgent severity) - """ - try: - # Gateway routes /tenants/{tenant_id}/alerts/... to alert_processor service - return await self.get( - "/alerts/summary", - tenant_id=tenant_id - ) - except Exception as e: - logger.error("Error fetching alerts summary", error=str(e), tenant_id=tenant_id) - return None