Spaces:
Sleeping
Sleeping
| """ | |
| CloudSRE v2 β Frontend Proxy Service (port 8004). | |
| A REAL reverse proxy that routes requests to payment and auth services. | |
| This is the entry point β when end users get errors, they hit this service. | |
| The frontend proxy: | |
| - Routes /api/pay β payment:8001/pay | |
| - Routes /api/auth β auth:8002/auth/token | |
| - Aggregates health from all upstream services | |
| - Returns real 502 Bad Gateway when upstreams are down | |
| The agent often sees frontend errors FIRST (502s), but the root cause | |
| is usually in payment, auth, or the database. This tests whether the | |
| agent investigates upstream instead of trying to fix frontend. | |
| """ | |
| import os | |
| import time | |
| import httpx | |
| from typing import Optional | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse | |
| from services.base_service import BaseService | |
| class FrontendProxy(BaseService): | |
| """Real reverse proxy microservice. | |
| Routes: | |
| GET / β Frontend status page | |
| POST /api/pay β Proxy to payment:8001/pay | |
| POST /api/auth β Proxy to auth:8002/auth/token | |
| GET /api/auth/verify β Proxy to auth:8002/auth/verify | |
| GET /upstream/health β Aggregated upstream health | |
| GET /healthz β (inherited) | |
| GET /metrics β (inherited) | |
| """ | |
| def __init__( | |
| self, | |
| port: int = 8004, | |
| log_dir: str = "/var/log", | |
| payment_url: str = "http://localhost:8001", | |
| auth_url: str = "http://localhost:8002", | |
| worker_url: str = "http://localhost:8003", | |
| ): | |
| super().__init__("frontend", port=port, log_dir=log_dir) | |
| self._pid = os.getpid() | |
| self._payment_url = payment_url | |
| self._auth_url = auth_url | |
| self._worker_url = worker_url | |
| # Frontend-specific metrics | |
| self.proxy_errors = self.metrics.add_counter("proxy_errors") | |
| self.upstream_502s = self.metrics.add_counter("upstream_502_count") | |
| self._setup_routes() | |
| def _setup_routes(self): | |
| async def index(): | |
| """Frontend status page.""" | |
| return { | |
| "service": "frontend-proxy", | |
| "version": "1.0.0", | |
| "routes": [ | |
| "POST /api/pay β payment service", | |
| "POST /api/auth β auth service", | |
| "GET /api/auth/verify β auth service", | |
| "GET /upstream/health β aggregated health", | |
| ], | |
| } | |
| async def proxy_pay(request: Request): | |
| """Proxy payment requests to payment service.""" | |
| return await self._proxy_request("POST", f"{self._payment_url}/pay", request) | |
| async def proxy_auth(request: Request): | |
| """Proxy auth requests to auth service.""" | |
| return await self._proxy_request("POST", f"{self._auth_url}/auth/token", request) | |
| async def proxy_auth_verify(request: Request): | |
| """Proxy auth verify requests.""" | |
| return await self._proxy_request("GET", f"{self._auth_url}/auth/verify", request) | |
| async def upstream_health(): | |
| """Check health of ALL upstream services. | |
| This is what the agent should check FIRST β it shows | |
| which upstream services are failing. | |
| """ | |
| health = {} | |
| upstream_services = { | |
| "payment": f"{self._payment_url}/healthz", | |
| "auth": f"{self._auth_url}/healthz", | |
| "worker": f"{self._worker_url}/healthz", | |
| } | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| for name, url in upstream_services.items(): | |
| try: | |
| resp = await client.get(url) | |
| health[name] = { | |
| "status": "healthy" if resp.status_code == 200 else "unhealthy", | |
| "status_code": resp.status_code, | |
| "response": resp.json() if resp.status_code == 200 else None, | |
| } | |
| except httpx.ConnectError: | |
| health[name] = {"status": "unreachable", "error": "Connection refused"} | |
| except httpx.TimeoutException: | |
| health[name] = {"status": "timeout", "error": "Health check timed out"} | |
| except Exception as e: | |
| health[name] = {"status": "error", "error": str(e)} | |
| all_healthy = all(s["status"] == "healthy" for s in health.values()) | |
| return { | |
| "overall": "healthy" if all_healthy else "DEGRADED", | |
| "services": health, | |
| } | |
| async def _proxy_request(self, method: str, url: str, request: Request) -> JSONResponse: | |
| """Forward a request to an upstream service.""" | |
| try: | |
| headers = dict(request.headers) | |
| # Remove host header to avoid conflicts | |
| headers.pop("host", None) | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| if method == "POST": | |
| try: | |
| body = await request.body() | |
| except Exception: | |
| body = b"{}" | |
| resp = await client.post(url, content=body, headers=headers) | |
| else: | |
| resp = await client.get(url, headers=headers) | |
| # Return upstream response as-is | |
| try: | |
| return JSONResponse( | |
| status_code=resp.status_code, | |
| content=resp.json(), | |
| ) | |
| except Exception: | |
| return JSONResponse( | |
| status_code=resp.status_code, | |
| content={"raw": resp.text[:500]}, | |
| ) | |
| except httpx.ConnectError: | |
| self.proxy_errors.inc() | |
| self.upstream_502s.inc() | |
| self.logger.error(f"Upstream connection refused: {url}") | |
| return JSONResponse( | |
| status_code=502, | |
| content={"error": f"Bad Gateway β upstream {url} connection refused"}, | |
| ) | |
| except httpx.TimeoutException: | |
| self.proxy_errors.inc() | |
| self.logger.error(f"Upstream timeout: {url}") | |
| return JSONResponse( | |
| status_code=504, | |
| content={"error": f"Gateway Timeout β upstream {url} timed out"}, | |
| ) | |
| except Exception as e: | |
| self.proxy_errors.inc() | |
| self.logger.error(f"Proxy error: {e}", {"url": url}) | |
| return JSONResponse( | |
| status_code=502, | |
| content={"error": f"Bad Gateway β {str(e)}"}, | |
| ) | |
| def reset(self): | |
| super().reset() | |
| self._pid = os.getpid() | |