From 02daad41be99ab4121b5ccefa13059e78d41b0ae Mon Sep 17 00:00:00 2001 From: amberly-d Date: Mon, 22 Jun 2026 12:31:08 +0000 Subject: [PATCH 1/3] obs: Add Prometheus metrics endpoint for request counting and latency - Add prometheus-client==0.21.1 dependency - Add PrometheusMiddleware tracking http_requests_total, http_request_duration_seconds, http_requests_in_flight - Expose /metrics endpoint Closes #82 --- quantara/pyproject.toml | 1 + quantara/web_app/api/main.py | 3 ++ quantara/web_app/api/metrics.py | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 quantara/web_app/api/metrics.py diff --git a/quantara/pyproject.toml b/quantara/pyproject.toml index 791e57f8..6989f614 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" [tool.poetry.group.dev.dependencies] black = "24.8.0" isort = "5.13.2" diff --git a/quantara/web_app/api/main.py b/quantara/web_app/api/main.py index 3ca0a9a9..83b7f01c 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 @@ -141,6 +142,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.get("/health", tags=["Health"], summary="Health check endpoint") @@ -190,3 +192,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) From b2e81233b934d8f974fcccc2d84d87c8711b83a6 Mon Sep 17 00:00:00 2001 From: amberly-d Date: Tue, 23 Jun 2026 06:49:50 +0000 Subject: [PATCH 2/3] chore: regenerate poetry.lock with prometheus-client dependency --- quantara/poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantara/poetry.lock b/quantara/poetry.lock index c63e8dd0..ec8c59b4 100644 --- a/quantara/poetry.lock +++ b/quantara/poetry.lock @@ -4364,4 +4364,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.14" -content-hash = "6b33c2e509430cf12ec81fa653bd7c4ac97cc84706e9668a57b384e09ee72061" \ No newline at end of file +content-hash = "e72f5c566774ba9ded08d59637e76eb871cc3711990a0a4127a4acf23f17d32e" From 9d072bcabbc27823c3991ca266b315f8022b9474 Mon Sep 17 00:00:00 2001 From: amberly-d Date: Tue, 23 Jun 2026 15:52:49 +0000 Subject: [PATCH 3/3] fix: update poetry.lock content-hash for prometheus-client + structlog --- quantara/poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantara/poetry.lock b/quantara/poetry.lock index c874d9c9..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 = "e72f5c566774ba9ded08d59637e76eb871cc3711990a0a4127a4acf23f17d32e" +content-hash = "e1eb113bfb3744ea3b9175ad84a11f8e9e9eaef3ebd30a2cfe98d2958a9a44fc"