CloudSRE-Environment / services /frontend_proxy.py
Harikishanth R
Fix critical ModuleNotFoundError by removing absolute cloud_sre_v2 imports
160d7db
Raw
History Blame Contribute Delete
7.03 kB
"""
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):
@self.app.get("/")
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",
],
}
@self.app.post("/api/pay")
async def proxy_pay(request: Request):
"""Proxy payment requests to payment service."""
return await self._proxy_request("POST", f"{self._payment_url}/pay", request)
@self.app.post("/api/auth")
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)
@self.app.get("/api/auth/verify")
async def proxy_auth_verify(request: Request):
"""Proxy auth verify requests."""
return await self._proxy_request("GET", f"{self._auth_url}/auth/verify", request)
@self.app.get("/upstream/health")
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()