- Add proxy routes for data ingestion service in API gateway - Register data-ingestion-service in SERVICES config - Update docker-compose to include data-ingestion-service and sensor-service dependencies - Fix import typo in sensor-service (contextual -> contextlib) - Update FTP credentials and environment variables for data-ingestion-service
446 lines
17 KiB
Python
446 lines
17 KiB
Python
"""
|
|
API Gateway for Energy Management Microservices
|
|
Central entry point that routes requests to appropriate microservices.
|
|
Port: 8000
|
|
"""
|
|
|
|
import asyncio
|
|
import aiohttp
|
|
from datetime import datetime
|
|
from fastapi import FastAPI, HTTPException, WebSocket, Depends, Request, Response
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from contextlib import asynccontextmanager
|
|
import logging
|
|
import json
|
|
from typing import Dict, Any, Optional
|
|
import os
|
|
|
|
from models import ServiceConfig, HealthResponse, GatewayStats
|
|
from service_registry import ServiceRegistry
|
|
from load_balancer import LoadBalancer
|
|
from auth_middleware import AuthMiddleware
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan manager"""
|
|
logger.info("API Gateway starting up...")
|
|
|
|
# Initialize service registry
|
|
await service_registry.initialize()
|
|
|
|
# Start health check task
|
|
asyncio.create_task(health_check_task())
|
|
|
|
logger.info("API Gateway startup complete")
|
|
|
|
yield
|
|
|
|
logger.info("API Gateway shutting down...")
|
|
await service_registry.close()
|
|
logger.info("API Gateway shutdown complete")
|
|
|
|
app = FastAPI(
|
|
title="Energy Management API Gateway",
|
|
description="Central API gateway for energy management microservices",
|
|
version="1.0.0",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Service registry and load balancer
|
|
service_registry = ServiceRegistry()
|
|
load_balancer = LoadBalancer()
|
|
auth_middleware = AuthMiddleware()
|
|
|
|
# Service configuration
|
|
SERVICES = {
|
|
"token-service": ServiceConfig(
|
|
name="token-service",
|
|
base_url=os.getenv("TOKEN_SERVICE_URL", "http://energy-token-service:8001"),
|
|
health_endpoint="/health",
|
|
auth_required=False
|
|
),
|
|
"battery-service": ServiceConfig(
|
|
name="battery-service",
|
|
base_url=os.getenv("BATTERY_SERVICE_URL", "http://energy-battery-service:8002"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"demand-response-service": ServiceConfig(
|
|
name="demand-response-service",
|
|
base_url=os.getenv("DEMAND_RESPONSE_SERVICE_URL", "http://energy-demand-response-service:8003"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"p2p-trading-service": ServiceConfig(
|
|
name="p2p-trading-service",
|
|
base_url=os.getenv("P2P_TRADING_SERVICE_URL", "http://energy-p2p-trading-service:8004"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"forecasting-service": ServiceConfig(
|
|
name="forecasting-service",
|
|
base_url=os.getenv("FORECASTING_SERVICE_URL", "http://energy-forecasting-service:8005"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"iot-control-service": ServiceConfig(
|
|
name="iot-control-service",
|
|
base_url=os.getenv("IOT_CONTROL_SERVICE_URL", "http://energy-iot-control-service:8006"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"sensor-service": ServiceConfig(
|
|
name="sensor-service",
|
|
base_url=os.getenv("SENSOR_SERVICE_URL", "http://energy-sensor-service:8007"),
|
|
health_endpoint="/health",
|
|
auth_required=True
|
|
),
|
|
"data-ingestion-service": ServiceConfig(
|
|
name="data-ingestion-service",
|
|
base_url=os.getenv("DATA_INGESTION_SERVICE_URL", "http://energy-data-ingestion-service:8008"),
|
|
health_endpoint="/health",
|
|
auth_required=False
|
|
)
|
|
}
|
|
|
|
# Request statistics
|
|
request_stats = {
|
|
"total_requests": 0,
|
|
"successful_requests": 0,
|
|
"failed_requests": 0,
|
|
"service_requests": {service: 0 for service in SERVICES.keys()},
|
|
"start_time": datetime.utcnow()
|
|
}
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def gateway_health_check():
|
|
"""Gateway health check endpoint"""
|
|
try:
|
|
# Check all services
|
|
service_health = await service_registry.get_all_service_health()
|
|
|
|
healthy_services = sum(1 for status in service_health.values() if status.get("status") == "healthy")
|
|
total_services = len(SERVICES)
|
|
|
|
overall_status = "healthy" if healthy_services == total_services else "degraded"
|
|
|
|
return HealthResponse(
|
|
service="api-gateway",
|
|
status=overall_status,
|
|
timestamp=datetime.utcnow(),
|
|
version="1.0.0",
|
|
services=service_health,
|
|
healthy_services=healthy_services,
|
|
total_services=total_services
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Gateway health check failed: {e}")
|
|
raise HTTPException(status_code=503, detail="Service Unavailable")
|
|
|
|
@app.get("/services/status")
|
|
async def get_services_status():
|
|
"""Get status of all registered services"""
|
|
try:
|
|
service_health = await service_registry.get_all_service_health()
|
|
return {
|
|
"services": service_health,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"total_services": len(SERVICES),
|
|
"healthy_services": sum(1 for status in service_health.values() if status.get("status") == "healthy")
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting services status: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/stats", response_model=GatewayStats)
|
|
async def get_gateway_stats():
|
|
"""Get API gateway statistics"""
|
|
uptime = (datetime.utcnow() - request_stats["start_time"]).total_seconds()
|
|
|
|
return GatewayStats(
|
|
total_requests=request_stats["total_requests"],
|
|
successful_requests=request_stats["successful_requests"],
|
|
failed_requests=request_stats["failed_requests"],
|
|
success_rate=round((request_stats["successful_requests"] / max(request_stats["total_requests"], 1)) * 100, 2),
|
|
uptime_seconds=uptime,
|
|
service_requests=request_stats["service_requests"],
|
|
timestamp=datetime.utcnow()
|
|
)
|
|
|
|
# Token Service Routes
|
|
@app.api_route("/api/v1/tokens/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def token_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to token service"""
|
|
return await proxy_request(request, "token-service", f"/{path}")
|
|
|
|
# Battery Service Routes
|
|
@app.api_route("/api/v1/batteries/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def battery_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to battery service"""
|
|
return await proxy_request(request, "battery-service", f"/{path}")
|
|
|
|
# Demand Response Service Routes
|
|
@app.api_route("/api/v1/demand-response/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def demand_response_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to demand response service"""
|
|
return await proxy_request(request, "demand-response-service", f"/{path}")
|
|
|
|
# P2P Trading Service Routes
|
|
@app.api_route("/api/v1/p2p/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def p2p_trading_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to P2P trading service"""
|
|
return await proxy_request(request, "p2p-trading-service", f"/{path}")
|
|
|
|
# Forecasting Service Routes
|
|
@app.api_route("/api/v1/forecast/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def forecasting_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to forecasting service"""
|
|
return await proxy_request(request, "forecasting-service", f"/{path}")
|
|
|
|
# IoT Control Service Routes
|
|
@app.api_route("/api/v1/iot/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def iot_control_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to IoT control service"""
|
|
return await proxy_request(request, "iot-control-service", f"/{path}")
|
|
|
|
# Sensor Service Routes (Original Dashboard Functionality)
|
|
@app.api_route("/api/v1/sensors/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def sensor_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to sensor service"""
|
|
return await proxy_request(request, "sensor-service", f"/{path}")
|
|
|
|
# Data Ingestion Service Routes (SA4CPS FTP Monitoring)
|
|
@app.api_route("/api/v1/ingestion/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def data_ingestion_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to data ingestion service"""
|
|
return await proxy_request(request, "data-ingestion-service", f"/{path}")
|
|
|
|
@app.api_route("/api/v1/sources/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def data_sources_proxy(request: Request, path: str):
|
|
"""Proxy requests to data ingestion service for data sources"""
|
|
return await proxy_request(request, "data-ingestion-service", f"/sources/{path}")
|
|
|
|
@app.get("/api/v1/sources")
|
|
async def data_sources_list_proxy(request: Request):
|
|
"""Proxy requests to data ingestion service for sources list"""
|
|
return await proxy_request(request, "data-ingestion-service", "/sources")
|
|
|
|
@app.api_route("/api/v1/rooms/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
async def room_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to sensor service for room management"""
|
|
return await proxy_request(request, "sensor-service", f"/rooms/{path}")
|
|
|
|
@app.api_route("/api/v1/data/{path:path}", methods=["GET", "POST"])
|
|
async def data_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to sensor service for data operations"""
|
|
return await proxy_request(request, "sensor-service", f"/data/{path}")
|
|
|
|
@app.api_route("/api/v1/analytics/{path:path}", methods=["GET", "POST"])
|
|
async def analytics_service_proxy(request: Request, path: str):
|
|
"""Proxy requests to sensor service for analytics"""
|
|
return await proxy_request(request, "sensor-service", f"/analytics/{path}")
|
|
|
|
@app.api_route("/api/v1/export", methods=["GET"])
|
|
async def export_service_proxy(request: Request):
|
|
"""Proxy requests to sensor service for data export"""
|
|
return await proxy_request(request, "sensor-service", "/export")
|
|
|
|
@app.api_route("/api/v1/events", methods=["GET"])
|
|
async def events_service_proxy(request: Request):
|
|
"""Proxy requests to sensor service for system events"""
|
|
return await proxy_request(request, "sensor-service", "/events")
|
|
|
|
# WebSocket proxy for real-time data
|
|
@app.websocket("/ws")
|
|
async def websocket_proxy(websocket: WebSocket):
|
|
"""Proxy WebSocket connections to sensor service"""
|
|
try:
|
|
# Get sensor service URL
|
|
service_url = await load_balancer.get_service_url("sensor-service")
|
|
if not service_url:
|
|
await websocket.close(code=1003, reason="Sensor service unavailable")
|
|
return
|
|
|
|
# For simplicity, we'll just accept the connection and forward to sensor service
|
|
# In a production setup, you'd want a proper WebSocket proxy
|
|
await websocket.accept()
|
|
|
|
# For now, we'll handle this by having the sensor service manage WebSockets directly
|
|
# The frontend should connect to the sensor service WebSocket endpoint directly
|
|
await websocket.send_text(json.dumps({
|
|
"type": "proxy_info",
|
|
"message": "Connect directly to sensor service WebSocket at /ws",
|
|
"sensor_service_url": service_url.replace("http://", "ws://") + "/ws"
|
|
}))
|
|
|
|
# Keep connection alive
|
|
while True:
|
|
try:
|
|
await websocket.receive_text()
|
|
except:
|
|
break
|
|
|
|
except Exception as e:
|
|
logger.error(f"WebSocket proxy error: {e}")
|
|
|
|
async def proxy_request(request: Request, service_name: str, path: str):
|
|
"""Generic request proxy function"""
|
|
try:
|
|
# Update request statistics
|
|
request_stats["total_requests"] += 1
|
|
request_stats["service_requests"][service_name] += 1
|
|
|
|
# Get service configuration
|
|
service_config = SERVICES.get(service_name)
|
|
if not service_config:
|
|
raise HTTPException(status_code=404, detail=f"Service {service_name} not found")
|
|
|
|
# Check authentication if required
|
|
if service_config.auth_required:
|
|
await auth_middleware.verify_token(request)
|
|
|
|
# Get healthy service instance
|
|
service_url = await load_balancer.get_service_url(service_name)
|
|
|
|
# Prepare request
|
|
url = f"{service_url}{path}"
|
|
method = request.method
|
|
headers = dict(request.headers)
|
|
|
|
# Remove hop-by-hop headers
|
|
headers.pop("host", None)
|
|
headers.pop("content-length", None)
|
|
|
|
# Get request body
|
|
body = None
|
|
if method in ["POST", "PUT", "PATCH"]:
|
|
body = await request.body()
|
|
|
|
# Make request to service
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.request(
|
|
method=method,
|
|
url=url,
|
|
headers=headers,
|
|
data=body,
|
|
params=dict(request.query_params),
|
|
timeout=aiohttp.ClientTimeout(total=30)
|
|
) as response:
|
|
|
|
# Get response data
|
|
response_data = await response.read()
|
|
response_headers = dict(response.headers)
|
|
|
|
# Remove hop-by-hop headers from response
|
|
response_headers.pop("transfer-encoding", None)
|
|
response_headers.pop("connection", None)
|
|
|
|
# Update success statistics
|
|
if response.status < 400:
|
|
request_stats["successful_requests"] += 1
|
|
else:
|
|
request_stats["failed_requests"] += 1
|
|
|
|
# Return response
|
|
return Response(
|
|
content=response_data,
|
|
status_code=response.status,
|
|
headers=response_headers,
|
|
media_type=response_headers.get("content-type")
|
|
)
|
|
|
|
except aiohttp.ClientError as e:
|
|
request_stats["failed_requests"] += 1
|
|
logger.error(f"Service {service_name} connection error: {e}")
|
|
raise HTTPException(status_code=503, detail=f"Service {service_name} unavailable")
|
|
|
|
except HTTPException:
|
|
request_stats["failed_requests"] += 1
|
|
raise
|
|
|
|
except Exception as e:
|
|
request_stats["failed_requests"] += 1
|
|
logger.error(f"Proxy error for {service_name}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal gateway error")
|
|
|
|
@app.get("/api/v1/overview")
|
|
async def get_system_overview():
|
|
"""Get comprehensive system overview from all services"""
|
|
try:
|
|
overview = {}
|
|
|
|
# Get data from each service
|
|
for service_name in SERVICES.keys():
|
|
try:
|
|
if await service_registry.is_service_healthy(service_name):
|
|
service_url = await load_balancer.get_service_url(service_name)
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
# Try to get service-specific overview data
|
|
overview_endpoints = {
|
|
"sensor-service": "/analytics/summary",
|
|
"battery-service": "/batteries",
|
|
"demand-response-service": "/flexibility/current",
|
|
"p2p-trading-service": "/market/status",
|
|
"forecasting-service": "/forecast/summary",
|
|
"iot-control-service": "/devices/summary"
|
|
}
|
|
|
|
endpoint = overview_endpoints.get(service_name)
|
|
if endpoint:
|
|
async with session.get(f"{service_url}{endpoint}", timeout=aiohttp.ClientTimeout(total=5)) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
overview[service_name] = data
|
|
else:
|
|
overview[service_name] = {"status": "error", "message": "Service returned error"}
|
|
else:
|
|
overview[service_name] = {"status": "available"}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Could not get overview from {service_name}: {e}")
|
|
overview[service_name] = {"status": "unavailable", "error": str(e)}
|
|
|
|
return {
|
|
"system_overview": overview,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"services_checked": len(SERVICES)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting system overview: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
async def health_check_task():
|
|
"""Background task for periodic health checks"""
|
|
logger.info("Starting health check task")
|
|
|
|
while True:
|
|
try:
|
|
await service_registry.update_all_service_health()
|
|
await asyncio.sleep(30) # Check every 30 seconds
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in health check task: {e}")
|
|
await asyncio.sleep(60)
|
|
|
|
# Initialize service registry with services
|
|
asyncio.create_task(service_registry.register_services(SERVICES))
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|