This commit is contained in:
1
services/api_gateway/__init__.py
Normal file
1
services/api_gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API Gateway Package
|
||||
@@ -1,11 +1,13 @@
|
||||
from fastapi import FastAPI, HTTPException, Request, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from shared.config import settings
|
||||
import asyncio
|
||||
|
||||
app = FastAPI(title="API Gateway", version="1.0.0")
|
||||
|
||||
@@ -21,10 +23,10 @@ app.add_middleware(
|
||||
# Service registry
|
||||
SERVICES = {
|
||||
"users": "http://localhost:8001",
|
||||
"emergency": "http://localhost:8002",
|
||||
"emergency": "http://localhost:8002",
|
||||
"location": "http://localhost:8003",
|
||||
"calendar": "http://localhost:8004",
|
||||
"notifications": "http://localhost:8005"
|
||||
"notifications": "http://localhost:8005",
|
||||
}
|
||||
|
||||
# Rate limiting (simple in-memory implementation)
|
||||
@@ -45,40 +47,61 @@ def is_rate_limited(client_ip: str) -> bool:
|
||||
"""Check if client is rate limited"""
|
||||
current_time = int(time.time())
|
||||
window_start = current_time - RATE_LIMIT_WINDOW
|
||||
|
||||
|
||||
if client_ip not in request_counts:
|
||||
request_counts[client_ip] = {}
|
||||
|
||||
|
||||
# Clean old entries
|
||||
request_counts[client_ip] = {
|
||||
timestamp: count for timestamp, count in request_counts[client_ip].items()
|
||||
timestamp: count
|
||||
for timestamp, count in request_counts[client_ip].items()
|
||||
if int(timestamp) > window_start
|
||||
}
|
||||
|
||||
|
||||
# Count requests in current window
|
||||
total_requests = sum(request_counts[client_ip].values())
|
||||
|
||||
|
||||
if total_requests >= RATE_LIMIT_REQUESTS:
|
||||
return True
|
||||
|
||||
|
||||
# Add current request
|
||||
timestamp_key = str(current_time)
|
||||
request_counts[client_ip][timestamp_key] = request_counts[client_ip].get(timestamp_key, 0) + 1
|
||||
|
||||
request_counts[client_ip][timestamp_key] = (
|
||||
request_counts[client_ip].get(timestamp_key, 0) + 1
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def proxy_request(service_url: str, path: str, method: str, headers: dict, body: bytes = None, params: dict = None):
|
||||
async def proxy_request(
|
||||
service_url: str,
|
||||
path: str,
|
||||
method: str,
|
||||
headers: dict,
|
||||
body: bytes = None,
|
||||
params: dict = None,
|
||||
):
|
||||
"""Proxy request to microservice"""
|
||||
url = f"{service_url}{path}"
|
||||
|
||||
|
||||
# Remove hop-by-hop headers
|
||||
filtered_headers = {
|
||||
k: v for k, v in headers.items()
|
||||
if k.lower() not in ["host", "connection", "upgrade", "proxy-connection",
|
||||
"proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding"]
|
||||
k: v
|
||||
for k, v in headers.items()
|
||||
if k.lower()
|
||||
not in [
|
||||
"host",
|
||||
"connection",
|
||||
"upgrade",
|
||||
"proxy-connection",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
@@ -86,7 +109,7 @@ async def proxy_request(service_url: str, path: str, method: str, headers: dict,
|
||||
url=url,
|
||||
headers=filtered_headers,
|
||||
content=body,
|
||||
params=params
|
||||
params=params,
|
||||
)
|
||||
return response
|
||||
except httpx.TimeoutException:
|
||||
@@ -101,17 +124,14 @@ async def proxy_request(service_url: str, path: str, method: str, headers: dict,
|
||||
async def rate_limiting_middleware(request: Request, call_next):
|
||||
"""Rate limiting middleware"""
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
|
||||
# Skip rate limiting for health checks
|
||||
if request.url.path.endswith("/health"):
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
if is_rate_limited(client_ip):
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={"detail": "Rate limit exceeded"}
|
||||
)
|
||||
|
||||
return JSONResponse(status_code=429, content={"detail": "Rate limit exceeded"})
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
@@ -128,12 +148,16 @@ async def user_service_proxy(request: Request):
|
||||
request.method,
|
||||
dict(request.headers),
|
||||
body,
|
||||
dict(request.query_params)
|
||||
dict(request.query_params),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=response.json(),
|
||||
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
|
||||
headers={
|
||||
k: v
|
||||
for k, v in response.headers.items()
|
||||
if k.lower() not in ["content-length", "transfer-encoding"]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -152,12 +176,16 @@ async def emergency_service_proxy(request: Request):
|
||||
request.method,
|
||||
dict(request.headers),
|
||||
body,
|
||||
dict(request.query_params)
|
||||
dict(request.query_params),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=response.json(),
|
||||
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
|
||||
headers={
|
||||
k: v
|
||||
for k, v in response.headers.items()
|
||||
if k.lower() not in ["content-length", "transfer-encoding"]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -176,12 +204,16 @@ async def location_service_proxy(request: Request):
|
||||
request.method,
|
||||
dict(request.headers),
|
||||
body,
|
||||
dict(request.query_params)
|
||||
dict(request.query_params),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=response.json(),
|
||||
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
|
||||
headers={
|
||||
k: v
|
||||
for k, v in response.headers.items()
|
||||
if k.lower() not in ["content-length", "transfer-encoding"]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -199,12 +231,16 @@ async def calendar_service_proxy(request: Request):
|
||||
request.method,
|
||||
dict(request.headers),
|
||||
body,
|
||||
dict(request.query_params)
|
||||
dict(request.query_params),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=response.json(),
|
||||
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
|
||||
headers={
|
||||
k: v
|
||||
for k, v in response.headers.items()
|
||||
if k.lower() not in ["content-length", "transfer-encoding"]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -222,12 +258,16 @@ async def notification_service_proxy(request: Request):
|
||||
request.method,
|
||||
dict(request.headers),
|
||||
body,
|
||||
dict(request.query_params)
|
||||
dict(request.query_params),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=response.json(),
|
||||
headers={k: v for k, v in response.headers.items() if k.lower() not in ["content-length", "transfer-encoding"]}
|
||||
headers={
|
||||
k: v
|
||||
for k, v in response.headers.items()
|
||||
if k.lower() not in ["content-length", "transfer-encoding"]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -241,7 +281,7 @@ async def gateway_health_check():
|
||||
async def check_services_status():
|
||||
"""Check status of all microservices"""
|
||||
service_status = {}
|
||||
|
||||
|
||||
async def check_service(name: str, url: str):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
@@ -249,25 +289,23 @@ async def check_services_status():
|
||||
service_status[name] = {
|
||||
"status": "healthy" if response.status_code == 200 else "unhealthy",
|
||||
"response_time_ms": response.elapsed.total_seconds() * 1000,
|
||||
"url": url
|
||||
"url": url,
|
||||
}
|
||||
except Exception as e:
|
||||
service_status[name] = {
|
||||
"status": "unhealthy",
|
||||
"error": str(e),
|
||||
"url": url
|
||||
}
|
||||
|
||||
service_status[name] = {"status": "unhealthy", "error": str(e), "url": url}
|
||||
|
||||
# Check all services concurrently
|
||||
tasks = [check_service(name, url) for name, url in SERVICES.items()]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
all_healthy = all(status["status"] == "healthy" for status in service_status.values())
|
||||
|
||||
|
||||
all_healthy = all(
|
||||
status["status"] == "healthy" for status in service_status.values()
|
||||
)
|
||||
|
||||
return {
|
||||
"gateway_status": "healthy",
|
||||
"all_services_healthy": all_healthy,
|
||||
"services": service_status
|
||||
"services": service_status,
|
||||
}
|
||||
|
||||
|
||||
@@ -284,12 +322,13 @@ async def root():
|
||||
"emergency": "/api/v1/alert, /api/v1/alerts/*",
|
||||
"location": "/api/v1/update-location, /api/v1/nearby-users",
|
||||
"calendar": "/api/v1/entries, /api/v1/cycle-overview",
|
||||
"notifications": "/api/v1/register-device, /api/v1/send-notification"
|
||||
"notifications": "/api/v1/register-device, /api/v1/send-notification",
|
||||
},
|
||||
"docs": "/docs"
|
||||
"docs": "/docs",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user