feat: Update .env.example and Docker Compose configurations to enhance service performance and health checks. Adjust OCR settings for improved document processing efficiency, and refine API Gateway health checks for better service monitoring. Revise desktop application documentation to reflect new API Gateway URL and installation instructions.
This commit is contained in:
@@ -6,6 +6,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Enruta las peticiones del cliente hacia los microservicios internos sin lógica
|
||||
|
||||
## Puerto
|
||||
|
||||
**8000**
|
||||
**8189** en el host (mapeado al **8000** interno del contenedor). Es el único puerto publicado por `docker-compose`.
|
||||
|
||||
---
|
||||
|
||||
@@ -15,6 +15,7 @@ Enruta las peticiones del cliente hacia los microservicios internos sin lógica
|
||||
|
||||
| Ruta cliente | Servicio destino | Descripción |
|
||||
|--------------|------------------|-------------|
|
||||
| `GET /health/live` | *(local)* | Liveness sin consultar upstreams |
|
||||
| `GET /health` | Agrega `/health` de los tres servicios | Estado del sistema |
|
||||
| `GET /models` | `extraction:8001/models` | Modelos LLM disponibles |
|
||||
| `GET /config` | `extraction:8001/config` + `cleaning:8002/rules` | Configuración |
|
||||
@@ -26,7 +27,7 @@ Enruta las peticiones del cliente hacia los microservicios internos sin lógica
|
||||
| `/count/{path}` | `count-api:8080/{path}` | Conteo OCR+ML (quita prefijo) |
|
||||
|
||||
El proxy reenvía método, query string, headers y body sin modificaciones.
|
||||
Timeout: ilimitado (para uploads grandes).
|
||||
Timeouts: JSON 30s lectura; uploads `/count/*` e ingest 600s.
|
||||
|
||||
---
|
||||
|
||||
@@ -63,4 +64,4 @@ requirements.txt — fastapi, httpx, uvicorn
|
||||
docker compose up --build api_gateway
|
||||
```
|
||||
|
||||
API disponible en `http://localhost:8000`.
|
||||
API disponible en `http://127.0.0.1:8189` (único puerto expuesto por docker-compose).
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
"""Gateway HTTP sin compartir codigo interno de los microservicios."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
_log = logging.getLogger("api_gateway")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
|
||||
|
||||
EXTRACTION_URL = os.getenv("EXTRACTION_URL", "http://extraction:8001").rstrip("/")
|
||||
CLEANING_URL = os.getenv("CLEANING_URL", "http://cleaning:8002").rstrip("/")
|
||||
COUNT_URL = os.getenv("COUNT_URL", "http://count-api:8080").rstrip("/")
|
||||
|
||||
app = FastAPI(title="IPSExtractor API Gateway", version="1.0.0")
|
||||
# Timeouts: JSON ligero vs uploads OCR/conteo
|
||||
_TIMEOUT_JSON = httpx.Timeout(connect=5.0, read=30.0, write=30.0, pool=5.0)
|
||||
_TIMEOUT_UPLOAD = httpx.Timeout(connect=10.0, read=600.0, write=600.0, pool=10.0)
|
||||
|
||||
_http_client: httpx.AsyncClient | None = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global _http_client
|
||||
_http_client = httpx.AsyncClient(
|
||||
timeout=_TIMEOUT_JSON,
|
||||
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
||||
)
|
||||
_log.info("Gateway iniciado — extraction=%s cleaning=%s count=%s", EXTRACTION_URL, CLEANING_URL, COUNT_URL)
|
||||
yield
|
||||
await _http_client.aclose()
|
||||
_http_client = None
|
||||
|
||||
|
||||
app = FastAPI(title="IPSExtractor API Gateway", version="1.0.0", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -24,12 +50,41 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
start = time.perf_counter()
|
||||
response = await call_next(request)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
_log.info("%s %s -> %s (%.0fms)", request.method, request.url.path, response.status_code, elapsed_ms)
|
||||
return response
|
||||
|
||||
|
||||
def _client() -> httpx.AsyncClient:
|
||||
if _http_client is None:
|
||||
raise RuntimeError("HTTP client no inicializado")
|
||||
return _http_client
|
||||
|
||||
|
||||
def _is_upload_path(path: str) -> bool:
|
||||
return path.startswith("/count/") or "/ingest" in path or "/upload" in path
|
||||
|
||||
|
||||
def _timeout_for_path(path: str) -> httpx.Timeout:
|
||||
return _TIMEOUT_UPLOAD if _is_upload_path(path) else _TIMEOUT_JSON
|
||||
|
||||
|
||||
@app.get("/health/live")
|
||||
async def health_live() -> dict[str, str]:
|
||||
"""Liveness sin consultar upstreams (para healthcheck de Docker)."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
extraction = await _safe_health(client, EXTRACTION_URL)
|
||||
cleaning = await _safe_health(client, CLEANING_URL)
|
||||
counting = await _safe_health(client, COUNT_URL)
|
||||
client = _client()
|
||||
extraction = await _safe_health(client, EXTRACTION_URL)
|
||||
cleaning = await _safe_health(client, CLEANING_URL)
|
||||
counting = await _safe_health(client, COUNT_URL)
|
||||
return {
|
||||
"status": "ok" if extraction["ok"] and cleaning["ok"] else "degraded",
|
||||
"services": {
|
||||
@@ -47,9 +102,9 @@ async def models() -> Response:
|
||||
|
||||
@app.get("/config")
|
||||
async def config() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
extraction = await _safe_json(client, f"{EXTRACTION_URL}/config")
|
||||
cleaning_rules = await _safe_json(client, f"{CLEANING_URL}/rules")
|
||||
client = _client()
|
||||
extraction = await _safe_json(client, f"{EXTRACTION_URL}/config")
|
||||
cleaning_rules = await _safe_json(client, f"{CLEANING_URL}/rules")
|
||||
return {
|
||||
"extraction": extraction,
|
||||
"cleaning": {"rules": cleaning_rules},
|
||||
@@ -91,7 +146,7 @@ async def count_jobs_proxy(path: str, request: Request) -> Response:
|
||||
return await _proxy_request(request, f"{EXTRACTION_URL}/count-jobs/{path}")
|
||||
|
||||
|
||||
@app.api_route("/jobs", methods=["GET"])
|
||||
@app.api_route("/jobs", methods=["GET", "POST"])
|
||||
async def jobs_root_proxy(request: Request) -> Response:
|
||||
return await _proxy_request(request, f"{EXTRACTION_URL}/jobs")
|
||||
|
||||
@@ -103,7 +158,7 @@ async def jobs_proxy(path: str, request: Request) -> Response:
|
||||
|
||||
async def _safe_health(client: httpx.AsyncClient, base_url: str) -> dict[str, Any]:
|
||||
try:
|
||||
response = await client.get(f"{base_url}/health")
|
||||
response = await client.get(f"{base_url}/health", timeout=_TIMEOUT_JSON)
|
||||
return {"ok": response.is_success, "status_code": response.status_code}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc)}
|
||||
@@ -111,7 +166,7 @@ async def _safe_health(client: httpx.AsyncClient, base_url: str) -> dict[str, An
|
||||
|
||||
async def _safe_json(client: httpx.AsyncClient, url: str) -> Any:
|
||||
try:
|
||||
response = await client.get(url)
|
||||
response = await client.get(url, timeout=_TIMEOUT_JSON)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as exc:
|
||||
@@ -119,8 +174,7 @@ async def _safe_json(client: httpx.AsyncClient, url: str) -> Any:
|
||||
|
||||
|
||||
async def _proxy(method: str, url: str) -> Response:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
upstream = await client.request(method, url)
|
||||
upstream = await _client().request(method, url, timeout=_TIMEOUT_JSON)
|
||||
return _to_response(upstream)
|
||||
|
||||
|
||||
@@ -131,15 +185,33 @@ async def _proxy_request(request: Request, url: str) -> Response:
|
||||
for key, value in request.headers.items()
|
||||
if key.lower() not in {"host", "content-length"}
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=None) as client:
|
||||
upstream = await client.request(
|
||||
timeout = _timeout_for_path(request.url.path)
|
||||
try:
|
||||
upstream = await _client().request(
|
||||
request.method,
|
||||
url,
|
||||
params=request.query_params,
|
||||
content=body,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
return _to_response(upstream)
|
||||
except httpx.ConnectError as exc:
|
||||
_log.error("Upstream no alcanzable %s: %s", url, exc)
|
||||
return Response(
|
||||
content=json.dumps(
|
||||
{"detail": "Servicio de conteo no disponible. Puede estar reiniciándose; intente de nuevo."}
|
||||
),
|
||||
status_code=503,
|
||||
media_type="application/json",
|
||||
)
|
||||
except httpx.TimeoutException as exc:
|
||||
_log.error("Timeout upstream %s: %s", url, exc)
|
||||
return Response(
|
||||
content=json.dumps({"detail": "Tiempo de espera agotado al contactar el servicio upstream."}),
|
||||
status_code=504,
|
||||
media_type="application/json",
|
||||
)
|
||||
return _to_response(upstream)
|
||||
|
||||
|
||||
def _to_response(upstream: httpx.Response) -> Response:
|
||||
|
||||
Reference in New Issue
Block a user