diff --git a/quantara/poetry.lock b/quantara/poetry.lock index 5caa3471..a7225164 100644 --- a/quantara/poetry.lock +++ b/quantara/poetry.lock @@ -4376,4 +4376,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.14" -content-hash = "5fbe7e1f30e2b011dd8d6f5ad6c53c02fa6af0f9406aaf7615d8f4ccfeec5d8c" +content-hash = "e1eb113bfb3744ea3b9175ad84a11f8e9e9eaef3ebd30a2cfe98d2958a9a44fc" diff --git a/quantara/pyproject.toml b/quantara/pyproject.toml index 20f9a4fe..1ff4c4c2 100644 --- a/quantara/pyproject.toml +++ b/quantara/pyproject.toml @@ -34,6 +34,7 @@ faker = "^30.8.1" notebook = "^7.2.2" sentry-sdk = {extras = ["fastapi"], version = "^2.18.0"} slowapi = "^0.1.9" +prometheus-client = "0.21.1" structlog = ">=24.1.0" [tool.poetry.group.dev.dependencies] black = "24.8.0" diff --git a/quantara/web_app/api/main.py b/quantara/web_app/api/main.py index fb99576f..779b6646 100644 --- a/quantara/web_app/api/main.py +++ b/quantara/web_app/api/main.py @@ -31,6 +31,7 @@ from web_app.api.leaderboard import router as leaderboard_router from web_app.api.referal import router as referal_router from web_app.api.wallet_auth import router as auth_router +from web_app.api.metrics import router as metrics_router, PrometheusMiddleware from web_app.config_validator import assert_valid_config from web_app.api.middleware import MaxBodySizeMiddleware from web_app.db.database import init_db @@ -148,6 +149,7 @@ async def global_exception_handler(request: Request, exc: Exception): # full middleware stack and can reject requests before they reach routers. app.add_middleware(SlowAPIMiddleware) app.add_middleware(MaxBodySizeMiddleware, max_body_size=1024*1024) +app.add_middleware(PrometheusMiddleware) @app.middleware("http") @@ -207,3 +209,4 @@ async def health_check(response: Response, db: Session = Depends(get_database)): app.include_router(leaderboard_router) app.include_router(referal_router) app.include_router(auth_router) +app.include_router(metrics_router) diff --git a/quantara/web_app/api/metrics.py b/quantara/web_app/api/metrics.py new file mode 100644 index 00000000..7deb940b --- /dev/null +++ b/quantara/web_app/api/metrics.py @@ -0,0 +1,67 @@ +""" +Prometheus metrics endpoint and middleware for the QUANTARA API. + +Exposes /metrics with: +- http_requests_total: counter by method, endpoint, status +- http_request_duration_seconds: histogram of request latency +- http_requests_in_flight: gauge of concurrent requests +""" + +import time + +from fastapi import APIRouter +from prometheus_client import ( + Counter, + Gauge, + Histogram, + generate_latest, + CONTENT_TYPE_LATEST, +) +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + +router = APIRouter() + +REQUEST_COUNT = Counter( + "http_requests_total", + "Total HTTP request count", + ["method", "endpoint", "status"], +) +REQUEST_LATENCY = Histogram( + "http_request_duration_seconds", + "HTTP request latency in seconds", + ["method", "endpoint"], +) +REQUESTS_IN_FLIGHT = Gauge( + "http_requests_in_flight", + "Number of HTTP requests currently being processed", +) + + +class PrometheusMiddleware(BaseHTTPMiddleware): + """Middleware that records request count, latency, and in-flight gauge.""" + + async def dispatch(self, request: Request, call_next) -> Response: + endpoint = request.url.path + method = request.method + REQUESTS_IN_FLIGHT.inc() + start = time.perf_counter() + try: + response = await call_next(request) + status = str(response.status_code) + except Exception: + status = "500" + raise + finally: + duration = time.perf_counter() - start + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc() + REQUEST_LATENCY.labels(method=method, endpoint=endpoint).observe(duration) + REQUESTS_IN_FLIGHT.dec() + return response + + +@router.get("/metrics", tags=["Observability"], summary="Prometheus metrics endpoint") +async def metrics() -> Response: + """Expose Prometheus metrics in text format.""" + return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)