diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 7e67628..fca17fd 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -46,8 +46,10 @@ async def dispatch(self, request: Request, call_next) -> Response: # Log request logger.info(f"Request: {request.method} {request.url.path}") - # Skip auth for static files and configured auth endpoint - if request.url.path.startswith('/static') or request.url.path == self.auth_redirect_url: + # Skip auth for static files, health check, and configured auth endpoint + if (request.url.path.startswith('/static') or + request.url.path == '/api/health' or + request.url.path == self.auth_redirect_url): return await call_next(request) # Validate proxy secret if enabled (skip in debug mode for local development) @@ -115,7 +117,10 @@ async def dispatch(self, request: Request, call_next) -> Response: # Distinguish between API endpoints (return 401) and browser endpoints (redirect) if request.url.path.startswith('/api/'): logger.warning(f"Missing authentication for API endpoint: {request.url.path}") - raise HTTPException(status_code=401, detail="Unauthorized") + return JSONResponse( + status_code=401, + content={"detail": "Unauthorized"} + ) else: logger.warning(f"Missing {self.auth_header_name}, redirecting to {self.auth_redirect_url}") return RedirectResponse(url=self.auth_redirect_url, status_code=302) diff --git a/backend/main.py b/backend/main.py index 5de95e6..16449a9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -33,6 +33,8 @@ from routes.config_routes import router as config_router from routes.admin_routes import admin_router from routes.files_routes import router as files_router +from routes.health_routes import router as health_router +from version import VERSION # Load environment variables from the parent directory load_dotenv(dotenv_path="../.env") @@ -111,8 +113,8 @@ async def lifespan(app: FastAPI): # Create FastAPI app with minimal setup app = FastAPI( title="Chat UI Backend", - description="Basic chat backend with modular architecture", - version="2.0.0", + description="Basic chat backend with modular architecture", + version=VERSION, lifespan=lifespan, ) @@ -141,6 +143,7 @@ async def lifespan(app: FastAPI): app.include_router(config_router) app.include_router(admin_router) app.include_router(files_router) +app.include_router(health_router) # Serve frontend build (Vite) project_root = Path(__file__).resolve().parents[1] diff --git a/backend/routes/health_routes.py b/backend/routes/health_routes.py new file mode 100644 index 0000000..6c4b8fa --- /dev/null +++ b/backend/routes/health_routes.py @@ -0,0 +1,40 @@ +"""Health check routes for service monitoring and load balancing. + +Provides simple health check endpoint for monitoring tools, orchestrators, +and load balancers to verify service availability. +""" + +import logging +from datetime import datetime, timezone +from typing import Dict, Any + +from fastapi import APIRouter + +from version import VERSION + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api", tags=["health"]) + + +@router.get("/health") +async def health_check() -> Dict[str, Any]: + """Health check endpoint for service monitoring. + + Returns basic service status information. This endpoint does not require + authentication and is intended for use by load balancers, monitoring + systems, and orchestration platforms. + + Returns: + Dictionary containing: + - status: Service health status ("healthy") + - service: Service name + - version: Service version + - timestamp: Current UTC timestamp in ISO-8601 format + """ + return { + "status": "healthy", + "service": "atlas-ui-3-backend", + "version": VERSION, + "timestamp": datetime.now(timezone.utc).isoformat() + } diff --git a/backend/tests/test_health_route.py b/backend/tests/test_health_route.py new file mode 100644 index 0000000..d40761b --- /dev/null +++ b/backend/tests/test_health_route.py @@ -0,0 +1,49 @@ +"""Unit tests for health check endpoint.""" + +from starlette.testclient import TestClient +from datetime import datetime + +from main import app + + +def test_health_endpoint_returns_200(): + """Test that health endpoint returns 200 status.""" + client = TestClient(app) + resp = client.get("/api/health") + assert resp.status_code == 200 + + +def test_health_endpoint_no_auth_required(): + """Test that health endpoint works without authentication.""" + client = TestClient(app) + # No X-User-Email header provided + resp = client.get("/api/health") + assert resp.status_code == 200 + + +def test_health_endpoint_response_structure(): + """Test that health endpoint returns correct response structure.""" + client = TestClient(app) + resp = client.get("/api/health") + assert resp.status_code == 200 + + data = resp.json() + + # Verify all required fields are present + assert "status" in data + assert "service" in data + assert "version" in data + assert "timestamp" in data + + # Verify field values + assert data["status"] == "healthy" + assert data["service"] == "atlas-ui-3-backend" + + # This version number can change, so just check it's a non-empty string + assert isinstance(data["version"], str) and len(data["version"]) > 0 + + # Verify timestamp is valid ISO-8601 format + try: + datetime.fromisoformat(data["timestamp"]) + except ValueError: + assert False, f"Invalid timestamp format: {data['timestamp']}" diff --git a/backend/tests/test_middleware_auth.py b/backend/tests/test_middleware_auth.py index 21dab33..107e021 100644 --- a/backend/tests/test_middleware_auth.py +++ b/backend/tests/test_middleware_auth.py @@ -345,3 +345,35 @@ def ping(request: Request): assert resp.status_code == 200 assert resp.json()["user"] == "debug@example.com" + +def test_health_endpoint_bypasses_auth(): + """Test that /api/health endpoint bypasses authentication middleware.""" + app = FastAPI() + + @app.get("/api/health") + def health(): + return {"status": "healthy"} + + @app.get("/api/other") + def other(): + return {"data": "test"} + + # Add an /auth route to receive redirects + @app.get("/auth") + def auth(): + return {"login": True} + + # Add middleware with auth required (debug_mode=False) + app.add_middleware(AuthMiddleware, debug_mode=False) + client = TestClient(app) + + # Health endpoint should work without auth header + health_resp = client.get("/api/health") + assert health_resp.status_code == 200 + assert health_resp.json()["status"] == "healthy" + + # Other API endpoints should still require auth (return 401) + other_resp = client.get("/api/other") + assert other_resp.status_code == 401 + assert "Unauthorized" in other_resp.json()["detail"] + diff --git a/backend/version.py b/backend/version.py new file mode 100644 index 0000000..57c38ca --- /dev/null +++ b/backend/version.py @@ -0,0 +1,6 @@ +"""Application version constant. + +Single source of truth for the application version number. +""" + +VERSION = "0.1.0" diff --git a/docs/02_admin_guide.md b/docs/02_admin_guide.md index 068ab61..43c2dc8 100644 --- a/docs/02_admin_guide.md +++ b/docs/02_admin_guide.md @@ -576,6 +576,10 @@ It is essential to configure the location where the `app.jsonl` file is stored, ``` * **Default**: If this variable is not set, the application will attempt to create a `logs` directory in the project's root, which may not be desirable or possible in a production deployment. Ensure the specified directory exists and the application has the necessary permissions to write to it. +## Health Monitoring (Added 2025-11-21) + +The application provides a public health check endpoint at `/api/health` specifically designed for monitoring tools, load balancers, and orchestration platforms. This endpoint requires no authentication and returns a JSON response containing the service status, version, and current timestamp in ISO-8601 format. You can integrate this endpoint into your monitoring infrastructure (such as Kubernetes liveness/readiness probes, AWS ELB health checks, or Prometheus monitoring) to verify that the backend service is running and responding correctly. The endpoint is lightweight and does not check database connectivity or external dependencies, making it ideal for high-frequency health polling without impacting application performance. For more detailed system status information that includes configuration and component health, admin users can access the `/admin/system-status` endpoint, which requires authentication and admin group membership. + ## LLM Configuration (`llmconfig.yml`) The `llmconfig.yml` file is where you define all the Large Language Models that the application can use. The application uses the `LiteLLM` library, which allows it to connect to a wide variety of LLM providers. diff --git a/docs/archive/endpoint_summary.md b/docs/archive/endpoint_summary.md index 422eb53..9f4a478 100644 --- a/docs/archive/endpoint_summary.md +++ b/docs/archive/endpoint_summary.md @@ -1,7 +1,7 @@ # Endpoint Summary Generated: 2025-08-09 -Branch: feature/s3-file-storage +Last Updated: 2025-11-21 ## 1. Frontend-Used Endpoints (HTTP method inferred from usage / backend definition.) @@ -47,6 +47,7 @@ Branch: feature/s3-file-storage - GET /api/debug/servers - GET /healthz - GET /api/files/health +- GET /api/health (service health check for monitoring/load balancers, added 2025-11-21) - DELETE /api/feedback/{feedback_id} - GET /api/feedback/stats