Create FastAPI gateway for routing to microservices
- Created api-gateway application structure - Added pyproject.toml with FastAPI, httpx, and aitbc-core dependencies - Implemented main.py with routing logic to proxy requests to microservices - Added service registry for GPU, Marketplace, Agent, Trading, Governance, and Coordinator services - Configured middleware (request ID, performance logging, validation, error handling) - Created systemd service file for api-gateway - Added README.md with service registry information This completes Phase 4.2: Create FastAPI gateway for routing
This commit is contained in:
42
apps/api-gateway/README.md
Normal file
42
apps/api-gateway/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# AITBC API Gateway
|
||||
|
||||
Routes requests to AITBC microservices.
|
||||
|
||||
## Service Registry
|
||||
|
||||
The gateway routes requests to the following services:
|
||||
|
||||
- **GPU Service** (port 8101): `/gpu/*` → GPU resource management
|
||||
- **Marketplace Service** (port 8102): `/marketplace/*` → GPU marketplace
|
||||
- **Agent Service** (port 8103): `/agent/*` → Agent operations
|
||||
- **Trading Service** (port 8104): `/trading/*` → Trading operations
|
||||
- **Governance Service** (port 8105): `/governance/*` → Governance operations
|
||||
- **Coordinator API** (port 8000): `/coordinator/*` → Coordinator API (default)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd /opt/aitbc
|
||||
poetry install --with api-gateway
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# Development
|
||||
python -m api_gateway.main
|
||||
|
||||
# Production (systemd)
|
||||
sudo systemctl start api-gateway
|
||||
sudo systemctl enable api-gateway
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /health` - Health check
|
||||
- `GET /services` - List registered services
|
||||
- `/*` - Proxy all other requests to appropriate microservice
|
||||
|
||||
## Configuration
|
||||
|
||||
Service URLs are configured in `main.py` under the `SERVICES` dictionary.
|
||||
16
apps/api-gateway/api-gateway.service
Normal file
16
apps/api-gateway/api-gateway.service
Normal file
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=AITBC API Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aitbc
|
||||
WorkingDirectory=/opt/aitbc/apps/api-gateway
|
||||
Environment="PATH=/opt/aitbc/venv/bin"
|
||||
Environment="PYTHONPATH=/opt/aitbc/packages/py/aitbc-core/src:/opt/aitbc/apps/api-gateway/src:/opt/aitbc"
|
||||
ExecStart=/opt/aitbc/venv/bin/python -m api_gateway.main
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
23
apps/api-gateway/pyproject.toml
Normal file
23
apps/api-gateway/pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[project]
|
||||
name = "api-gateway"
|
||||
version = "0.1.0"
|
||||
description = "AITBC API Gateway for routing to microservices"
|
||||
authors = [
|
||||
{name = "AITBC Team", email = "team@aitbc.dev"}
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn>=0.24.0",
|
||||
"httpx>=0.25.0",
|
||||
"aitbc-core",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
packages = [
|
||||
{ include = "api_gateway", from = "src" }
|
||||
]
|
||||
6
apps/api-gateway/src/api_gateway/__init__.py
Normal file
6
apps/api-gateway/src/api_gateway/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
AITBC API Gateway
|
||||
Routes requests to microservices
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
174
apps/api-gateway/src/api_gateway/main.py
Normal file
174
apps/api-gateway/src/api_gateway/main.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
API Gateway main application
|
||||
Routes requests to microservices
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from aitbc import (
|
||||
configure_logging,
|
||||
get_logger,
|
||||
RequestIDMiddleware,
|
||||
PerformanceLoggingMiddleware,
|
||||
RequestValidationMiddleware,
|
||||
ErrorHandlerMiddleware,
|
||||
)
|
||||
|
||||
# Configure structured logging
|
||||
configure_logging(level="INFO")
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Service registry configuration
|
||||
SERVICES = {
|
||||
"gpu": {
|
||||
"base_url": "http://localhost:8101",
|
||||
"prefix": "/gpu",
|
||||
},
|
||||
"marketplace": {
|
||||
"base_url": "http://localhost:8102",
|
||||
"prefix": "/marketplace",
|
||||
},
|
||||
"agent": {
|
||||
"base_url": "http://localhost:8103",
|
||||
"prefix": "/agent",
|
||||
},
|
||||
"trading": {
|
||||
"base_url": "http://localhost:8104",
|
||||
"prefix": "/trading",
|
||||
},
|
||||
"governance": {
|
||||
"base_url": "http://localhost:8105",
|
||||
"prefix": "/governance",
|
||||
},
|
||||
"coordinator": {
|
||||
"base_url": "http://localhost:8000",
|
||||
"prefix": "/coordinator",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
"""Lifecycle events for the API Gateway."""
|
||||
logger.info("Starting API Gateway")
|
||||
yield
|
||||
logger.info("Shutting down API Gateway")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="AITBC API Gateway",
|
||||
description="Routes requests to AITBC microservices",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# Add middleware
|
||||
app.add_middleware(RequestIDMiddleware)
|
||||
app.add_middleware(PerformanceLoggingMiddleware)
|
||||
app.add_middleware(RequestValidationMiddleware, max_request_size=10*1024*1024)
|
||||
app.add_middleware(ErrorHandlerMiddleware)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "service": "api-gateway"}
|
||||
|
||||
|
||||
@app.get("/services")
|
||||
async def list_services() -> dict[str, dict[str, str]]:
|
||||
"""List registered services"""
|
||||
return {
|
||||
service_name: {"prefix": config["prefix"], "url": config["base_url"]}
|
||||
for service_name, config in SERVICES.items()
|
||||
}
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
|
||||
async def proxy_request(path: str, request: Request) -> Response:
|
||||
"""Proxy request to appropriate microservice"""
|
||||
# Determine which service should handle the request
|
||||
service_name = None
|
||||
for name, config in SERVICES.items():
|
||||
if path.startswith(config["prefix"].lstrip("/")):
|
||||
service_name = name
|
||||
break
|
||||
|
||||
if not service_name:
|
||||
# Default to coordinator-api for unknown paths
|
||||
service_name = "coordinator"
|
||||
|
||||
service_config = SERVICES[service_name]
|
||||
|
||||
# Build target URL
|
||||
target_path = path
|
||||
if path.startswith(service_config["prefix"]):
|
||||
target_path = path[len(service_config["prefix"]):].lstrip("/")
|
||||
|
||||
target_url = f"{service_config['base_url']}/{target_path}"
|
||||
if request.url.query:
|
||||
target_url += f"?{request.url.query}"
|
||||
|
||||
# Proxy the request
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# Forward headers (except host)
|
||||
headers = dict(request.headers)
|
||||
headers.pop("host", None)
|
||||
headers.pop("content-length", None)
|
||||
|
||||
# Forward the request
|
||||
if request.method == "GET":
|
||||
response = await client.get(target_url, headers=headers, params=request.query_params)
|
||||
elif request.method == "POST":
|
||||
body = await request.body()
|
||||
response = await client.post(target_url, headers=headers, content=body)
|
||||
elif request.method == "PUT":
|
||||
body = await request.body()
|
||||
response = await client.put(target_url, headers=headers, content=body)
|
||||
elif request.method == "DELETE":
|
||||
response = await client.delete(target_url, headers=headers)
|
||||
elif request.method == "PATCH":
|
||||
body = await request.body()
|
||||
response = await client.patch(target_url, headers=headers, content=body)
|
||||
elif request.method == "OPTIONS":
|
||||
response = await client.options(target_url, headers=headers)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=405,
|
||||
content={"error": "Method not allowed"}
|
||||
)
|
||||
|
||||
# Return the response
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(
|
||||
"Service unavailable",
|
||||
service=service_name,
|
||||
target_url=target_url,
|
||||
error=str(e),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={
|
||||
"error": {
|
||||
"type": "service_unavailable",
|
||||
"message": f"Service {service_name} is unavailable",
|
||||
"service": service_name,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||
Reference in New Issue
Block a user