From a62d26dd25fed2897ce8819a8b609b0685a08558 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 7 Jul 2026 23:09:26 +0200 Subject: [PATCH 1/8] feat: add optional prometheus metrics endpoint. --- pyproject.toml | 4 +++ src/stac_auth_proxy/app.py | 24 ++++++++++++-- src/stac_auth_proxy/config.py | 1 + tests/test_metrics.py | 62 +++++++++++++++++++++++++++++++++++ uv.lock | 30 ++++++++++++++++- 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/test_metrics.py diff --git a/pyproject.toml b/pyproject.toml index 98878ad..d06a16b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,9 @@ docs = [ lambda = [ "mangum>=0.19.0", ] +metrics = [ + "prometheus-fastapi-instrumentator>=8.0.2", +] [tool.coverage.run] branch = true @@ -101,6 +104,7 @@ dev = [ "pytest>=8.3.3", "ruff>=0.0.238", "starlette-cramjam>=0.4.0", + "prometheus-fastapi-instrumentator>=8.0.2", "types-simplejson", "types-attrs", ] diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 535a0db..706e5d5 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -34,6 +34,19 @@ logger = logging.getLogger(__name__) +def add_metrics(app: FastAPI) -> None: + """Add the Prometheus metrics endpoint to the app.""" + try: + from prometheus_fastapi_instrumentator import Instrumentator + except ImportError as exc: + raise RuntimeError( + "ENABLE_METRICS is enabled, but prometheus-fastapi-instrumentator " + "is not installed. Install stac-auth-proxy[metrics]." + ) from exc + + Instrumentator().instrument(app).expose(app) + + def configure_app( app: FastAPI, settings: Optional[Settings] = None, @@ -55,6 +68,7 @@ def configure_app( """ settings = settings or Settings(**settings_kwargs) + public_endpoints = settings.public_endpoints # # Route Handlers @@ -85,6 +99,10 @@ def configure_app( prefix=settings.healthz_prefix, ) + if settings.enable_metrics: + add_metrics(app) + public_endpoints = {**public_endpoints, r"^/metrics$": ["GET"]} + # # Middleware (order is important, last added = first to run) # @@ -93,7 +111,7 @@ def configure_app( app.add_middleware( AuthenticationExtensionMiddleware, default_public=settings.default_public, - public_endpoints=settings.public_endpoints, + public_endpoints=public_endpoints, private_endpoints=settings.private_endpoints, items_filter_path=( settings.items_filter_path if settings.items_filter else None @@ -112,7 +130,7 @@ def configure_app( OpenApiMiddleware, openapi_spec_path=settings.openapi_spec_endpoint, oidc_discovery_url=str(settings.oidc_discovery_url), - public_endpoints=settings.public_endpoints, + public_endpoints=public_endpoints, private_endpoints=settings.private_endpoints, default_public=settings.default_public, root_path=settings.root_path, @@ -153,7 +171,7 @@ def configure_app( app.add_middleware( EnforceAuthMiddleware, - public_endpoints=settings.public_endpoints, + public_endpoints=public_endpoints, private_endpoints=settings.private_endpoints, default_public=settings.default_public, oidc_discovery_url=settings.oidc_discovery_internal_url, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 82d6655..140a6b2 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -81,6 +81,7 @@ class Settings(BaseSettings): wait_for_upstream: bool = True check_conformance: bool = True enable_compression: bool = True + enable_metrics: bool = False proxy_options: bool = False cors: CorsSettings = Field(default_factory=CorsSettings) diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..2a933da --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,62 @@ +"""Tests for optional Prometheus metrics support.""" + +import builtins + +import pytest +from fastapi.testclient import TestClient +from test_configure_app import get_flattened_routes +from utils import AppFactory + +from stac_auth_proxy import Settings, create_app + +app_factory = AppFactory( + oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", +) + + +def test_metrics_disabled_by_default(source_api_server): + """The metrics endpoint is not registered unless explicitly enabled.""" + app = app_factory(upstream_url=source_api_server) + + assert "/metrics" not in get_flattened_routes(app) + + +def test_metrics_enabled_from_env(monkeypatch, source_api_server): + """ENABLE_METRICS=true exposes Prometheus text output.""" + monkeypatch.setenv("ENABLE_METRICS", "true") + settings = Settings( + upstream_url=source_api_server, + oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", + wait_for_upstream=False, + check_conformance=False, + ) + app = create_app(settings) + client = TestClient(app) + + response = client.get("/metrics") + + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/plain") + assert "# HELP" in response.text + + +def test_metrics_enabled_without_extra_fails(monkeypatch, source_api_server): + """Enabling metrics without the optional dependency fails clearly.""" + real_import = builtins.__import__ + + def raise_for_instrumentator(name, *args, **kwargs): + if name == "prometheus_fastapi_instrumentator": + raise ImportError("No module named prometheus_fastapi_instrumentator") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", raise_for_instrumentator) + settings = Settings( + upstream_url=source_api_server, + oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", + enable_metrics=True, + wait_for_upstream=False, + check_conformance=False, + ) + + with pytest.raises(RuntimeError, match=r"Install stac-auth-proxy\[metrics\]"): + create_app(settings) diff --git a/uv.lock b/uv.lock index a05fd43..dee2a6b 100644 --- a/uv.lock +++ b/uv.lock @@ -1577,6 +1577,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "8.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/e9/2065686d1dfa62296fdc158b6e8fd25b0cb3dca09b0632cabeb5ae81fe4d/prometheus_fastapi_instrumentator-8.0.2.tar.gz", hash = "sha256:3c252e748151768a7aefd66824a04a870144f71de48a67aed211749a9ca2a548", size = 21342, upload-time = "2026-06-23T09:39:31.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c7/fa2b3f469a2e6001b829e0d6bc8680755349aa1329a87bf48731e9d5d30a/prometheus_fastapi_instrumentator-8.0.2-py3-none-any.whl", hash = "sha256:746002ec1e2c58b93f61444e1d104de959a9463a6a3f1c8909ac3757e16c3866", size = 20549, upload-time = "2026-06-23T09:39:32.616Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2015,12 +2037,16 @@ docs = [ lambda = [ { name = "mangum" }, ] +metrics = [ + { name = "prometheus-fastapi-instrumentator" }, +] [package.dev-dependencies] dev = [ { name = "jwcrypto" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "prometheus-fastapi-instrumentator" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -2048,19 +2074,21 @@ requires-dist = [ { name = "mkdocs-api-autonav", marker = "extra == 'docs'", specifier = ">=0.3.0" }, { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'docs'", specifier = ">=9.6.16" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.30.0" }, + { name = "prometheus-fastapi-instrumentator", marker = "extra == 'metrics'", specifier = ">=8.0.2" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "starlette", specifier = ">=1.0.1" }, { name = "starlette-cramjam", specifier = ">=0.4.0" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] -provides-extras = ["docs", "lambda"] +provides-extras = ["docs", "lambda", "metrics"] [package.metadata.requires-dev] dev = [ { name = "jwcrypto", specifier = ">=1.5.6" }, { name = "mypy", specifier = ">=1.3.0" }, { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "pytest-cov", specifier = ">=5.0.0" }, From 83d234ad74a2edbe43594cd43a973737df5afc0a Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Thu, 9 Jul 2026 20:05:36 +0200 Subject: [PATCH 2/8] Moved endpoint to _mgtm/metrics and route to config. --- pyproject.toml | 5 +-- src/stac_auth_proxy/app.py | 28 +++++----------- src/stac_auth_proxy/config.py | 2 +- tests/test_metrics.py | 62 ----------------------------------- uv.lock | 10 ++---- 5 files changed, 13 insertions(+), 94 deletions(-) delete mode 100644 tests/test_metrics.py diff --git a/pyproject.toml b/pyproject.toml index d06a16b..7bc4deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "httpx[http2]>=0.28.0", "jinja2>=3.1.6", "pydantic-settings>=2.6.1", + "prometheus-fastapi-instrumentator>=8.0.2", "pyjwt>=2.10.1", "starlette>=1.0.1", "starlette-cramjam>=0.4.0", @@ -43,9 +44,6 @@ docs = [ lambda = [ "mangum>=0.19.0", ] -metrics = [ - "prometheus-fastapi-instrumentator>=8.0.2", -] [tool.coverage.run] branch = true @@ -104,7 +102,6 @@ dev = [ "pytest>=8.3.3", "ruff>=0.0.238", "starlette-cramjam>=0.4.0", - "prometheus-fastapi-instrumentator>=8.0.2", "types-simplejson", "types-attrs", ] diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 706e5d5..d3c949f 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -10,6 +10,7 @@ import httpx from fastapi import FastAPI +from prometheus_fastapi_instrumentator import Instrumentator from starlette.middleware.cors import CORSMiddleware from starlette_cramjam.middleware import CompressionMiddleware @@ -34,19 +35,6 @@ logger = logging.getLogger(__name__) -def add_metrics(app: FastAPI) -> None: - """Add the Prometheus metrics endpoint to the app.""" - try: - from prometheus_fastapi_instrumentator import Instrumentator - except ImportError as exc: - raise RuntimeError( - "ENABLE_METRICS is enabled, but prometheus-fastapi-instrumentator " - "is not installed. Install stac-auth-proxy[metrics]." - ) from exc - - Instrumentator().instrument(app).expose(app) - - def configure_app( app: FastAPI, settings: Optional[Settings] = None, @@ -68,7 +56,6 @@ def configure_app( """ settings = settings or Settings(**settings_kwargs) - public_endpoints = settings.public_endpoints # # Route Handlers @@ -99,9 +86,10 @@ def configure_app( prefix=settings.healthz_prefix, ) - if settings.enable_metrics: - add_metrics(app) - public_endpoints = {**public_endpoints, r"^/metrics$": ["GET"]} + if r"^/_mgmt/metrics" in settings.public_endpoints: + Instrumentator().instrument(app).expose( + app, endpoint="/_mgmt/metrics", include_in_schema=False + ) # # Middleware (order is important, last added = first to run) @@ -111,7 +99,7 @@ def configure_app( app.add_middleware( AuthenticationExtensionMiddleware, default_public=settings.default_public, - public_endpoints=public_endpoints, + public_endpoints=settings.public_endpoints, private_endpoints=settings.private_endpoints, items_filter_path=( settings.items_filter_path if settings.items_filter else None @@ -130,7 +118,7 @@ def configure_app( OpenApiMiddleware, openapi_spec_path=settings.openapi_spec_endpoint, oidc_discovery_url=str(settings.oidc_discovery_url), - public_endpoints=public_endpoints, + public_endpoints=settings.public_endpoints, private_endpoints=settings.private_endpoints, default_public=settings.default_public, root_path=settings.root_path, @@ -171,7 +159,7 @@ def configure_app( app.add_middleware( EnforceAuthMiddleware, - public_endpoints=public_endpoints, + public_endpoints=settings.public_endpoints, private_endpoints=settings.private_endpoints, default_public=settings.default_public, oidc_discovery_url=settings.oidc_discovery_internal_url, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 140a6b2..4620a8f 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -81,7 +81,6 @@ class Settings(BaseSettings): wait_for_upstream: bool = True check_conformance: bool = True enable_compression: bool = True - enable_metrics: bool = False proxy_options: bool = False cors: CorsSettings = Field(default_factory=CorsSettings) @@ -108,6 +107,7 @@ class Settings(BaseSettings): r"^/healthz": ["GET"], r"^/_mgmt/ping": ["GET"], r"^/_mgmt/health": ["GET"], + r"^/_mgmt/metrics": ["GET"], } private_endpoints: EndpointMethodsWithScope = { # https://github.com/stac-api-extensions/collection-transaction/blob/v1.0.0-beta.1/README.md#methods diff --git a/tests/test_metrics.py b/tests/test_metrics.py deleted file mode 100644 index 2a933da..0000000 --- a/tests/test_metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Tests for optional Prometheus metrics support.""" - -import builtins - -import pytest -from fastapi.testclient import TestClient -from test_configure_app import get_flattened_routes -from utils import AppFactory - -from stac_auth_proxy import Settings, create_app - -app_factory = AppFactory( - oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", -) - - -def test_metrics_disabled_by_default(source_api_server): - """The metrics endpoint is not registered unless explicitly enabled.""" - app = app_factory(upstream_url=source_api_server) - - assert "/metrics" not in get_flattened_routes(app) - - -def test_metrics_enabled_from_env(monkeypatch, source_api_server): - """ENABLE_METRICS=true exposes Prometheus text output.""" - monkeypatch.setenv("ENABLE_METRICS", "true") - settings = Settings( - upstream_url=source_api_server, - oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", - wait_for_upstream=False, - check_conformance=False, - ) - app = create_app(settings) - client = TestClient(app) - - response = client.get("/metrics") - - assert response.status_code == 200 - assert response.headers["content-type"].startswith("text/plain") - assert "# HELP" in response.text - - -def test_metrics_enabled_without_extra_fails(monkeypatch, source_api_server): - """Enabling metrics without the optional dependency fails clearly.""" - real_import = builtins.__import__ - - def raise_for_instrumentator(name, *args, **kwargs): - if name == "prometheus_fastapi_instrumentator": - raise ImportError("No module named prometheus_fastapi_instrumentator") - return real_import(name, *args, **kwargs) - - monkeypatch.setattr(builtins, "__import__", raise_for_instrumentator) - settings = Settings( - upstream_url=source_api_server, - oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", - enable_metrics=True, - wait_for_upstream=False, - check_conformance=False, - ) - - with pytest.raises(RuntimeError, match=r"Install stac-auth-proxy\[metrics\]"): - create_app(settings) diff --git a/uv.lock b/uv.lock index dee2a6b..ef8baf3 100644 --- a/uv.lock +++ b/uv.lock @@ -2016,6 +2016,7 @@ dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, { name = "jinja2" }, + { name = "prometheus-fastapi-instrumentator" }, { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "starlette" }, @@ -2037,16 +2038,12 @@ docs = [ lambda = [ { name = "mangum" }, ] -metrics = [ - { name = "prometheus-fastapi-instrumentator" }, -] [package.dev-dependencies] dev = [ { name = "jwcrypto" }, { name = "mypy" }, { name = "pre-commit" }, - { name = "prometheus-fastapi-instrumentator" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -2074,21 +2071,20 @@ requires-dist = [ { name = "mkdocs-api-autonav", marker = "extra == 'docs'", specifier = ">=0.3.0" }, { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'docs'", specifier = ">=9.6.16" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.30.0" }, - { name = "prometheus-fastapi-instrumentator", marker = "extra == 'metrics'", specifier = ">=8.0.2" }, + { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "starlette", specifier = ">=1.0.1" }, { name = "starlette-cramjam", specifier = ">=0.4.0" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] -provides-extras = ["docs", "lambda", "metrics"] +provides-extras = ["docs", "lambda"] [package.metadata.requires-dev] dev = [ { name = "jwcrypto", specifier = ">=1.5.6" }, { name = "mypy", specifier = ">=1.3.0" }, { name = "pre-commit", specifier = ">=3.5.0" }, - { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "pytest-cov", specifier = ">=5.0.0" }, From 511be54dfb658db278e08d640226a6b341c88063 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Thu, 9 Jul 2026 20:20:08 +0200 Subject: [PATCH 3/8] Added test. --- tests/test_configure_app.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_configure_app.py b/tests/test_configure_app.py index 2ba7e6b..40cae7d 100644 --- a/tests/test_configure_app.py +++ b/tests/test_configure_app.py @@ -1,6 +1,7 @@ """Tests for configuring an external FastAPI application.""" from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient from stac_auth_proxy import Settings, configure_app @@ -65,3 +66,21 @@ def test_configure_app_excludes_proxy_route(): routes = get_flattened_routes(app) assert settings.healthz_prefix in routes assert "/{path:path}" not in routes + + +def test_metrics_endpoint_returns_prometheus_output(): + """Metrics returns Prometheus exposition format when enabled in PUBLIC_ENDPOINTS.""" + app = FastAPI() + settings = Settings( + upstream_url="https://example.com", + oidc_discovery_url="https://example.com/.well-known/openid-configuration", + wait_for_upstream=False, + check_conformance=False, + ) + + configure_app(app, settings) + response = TestClient(app).get("/_mgmt/metrics") + + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/plain") + assert "# HELP" in response.text From fc241a85661d080a5ffd1c8598841a8cae4a93ec Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Mon, 13 Jul 2026 23:34:29 +0200 Subject: [PATCH 4/8] Make prometheus metrics endpoint optional. --- docs/user-guide/configuration.md | 5 ++++- docs/user-guide/getting-started.md | 2 ++ pyproject.toml | 5 ++++- src/stac_auth_proxy/app.py | 10 ++++++++-- tests/test_configure_app.py | 18 ++++++++++++++++++ uv.lock | 10 +++++++--- 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 57d8a73..ae82c3c 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -160,9 +160,12 @@ The application is configurable via environment variables. "^/docs/oauth2-redirect": ["GET"], "^/healthz": ["GET"], "^/_mgmt/ping": ["GET"], - "^/_mgmt/health": ["GET"] + "^/_mgmt/health": ["GET"], + "^/_mgmt/metrics": ["GET"] } ``` + > [!NOTE] + > `/_mgmt/metrics` is only served when the `metrics` extra is installed (`pip install stac-auth-proxy[metrics]`); otherwise it's automatically dropped from `PUBLIC_ENDPOINTS` at startup. ### `ENABLE_AUTHENTICATION_EXTENSION` diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index cdafeef..ec4d695 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -53,6 +53,8 @@ docker run -p 8000:8000 \ ```bash pip install stac-auth-proxy ``` + > [!TIP] + > To expose a Prometheus `/_mgmt/metrics` endpoint, install the `metrics` extra instead: `pip install stac-auth-proxy[metrics]` 2. Set environment variables: ```bash export UPSTREAM_URL=https://your-stac-api.com/stac diff --git a/pyproject.toml b/pyproject.toml index 7bc4deb..d06a16b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ dependencies = [ "httpx[http2]>=0.28.0", "jinja2>=3.1.6", "pydantic-settings>=2.6.1", - "prometheus-fastapi-instrumentator>=8.0.2", "pyjwt>=2.10.1", "starlette>=1.0.1", "starlette-cramjam>=0.4.0", @@ -44,6 +43,9 @@ docs = [ lambda = [ "mangum>=0.19.0", ] +metrics = [ + "prometheus-fastapi-instrumentator>=8.0.2", +] [tool.coverage.run] branch = true @@ -102,6 +104,7 @@ dev = [ "pytest>=8.3.3", "ruff>=0.0.238", "starlette-cramjam>=0.4.0", + "prometheus-fastapi-instrumentator>=8.0.2", "types-simplejson", "types-attrs", ] diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index d3c949f..18cd37c 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -10,8 +10,12 @@ import httpx from fastapi import FastAPI -from prometheus_fastapi_instrumentator import Instrumentator from starlette.middleware.cors import CORSMiddleware + +try: + from prometheus_fastapi_instrumentator import Instrumentator +except ImportError: + Instrumentator = None from starlette_cramjam.middleware import CompressionMiddleware from .config import Settings @@ -86,10 +90,12 @@ def configure_app( prefix=settings.healthz_prefix, ) - if r"^/_mgmt/metrics" in settings.public_endpoints: + if Instrumentator and r"^/_mgmt/metrics" in settings.public_endpoints: Instrumentator().instrument(app).expose( app, endpoint="/_mgmt/metrics", include_in_schema=False ) + else: + settings.public_endpoints.pop(r"^/_mgmt/metrics", None) # # Middleware (order is important, last added = first to run) diff --git a/tests/test_configure_app.py b/tests/test_configure_app.py index 40cae7d..2018e1d 100644 --- a/tests/test_configure_app.py +++ b/tests/test_configure_app.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +import stac_auth_proxy.app as app_module from stac_auth_proxy import Settings, configure_app @@ -84,3 +85,20 @@ def test_metrics_endpoint_returns_prometheus_output(): assert response.status_code == 200 assert response.headers["content-type"].startswith("text/plain") assert "# HELP" in response.text + + +def test_metrics_endpoint_skipped_without_instrumentator(monkeypatch): + """Metrics route and public endpoint are omitted when the extra is absent.""" + monkeypatch.setattr(app_module, "Instrumentator", None) + app = FastAPI() + settings = Settings( + upstream_url="https://example.com", + oidc_discovery_url="https://example.com/.well-known/openid-configuration", + wait_for_upstream=False, + check_conformance=False, + ) + + configure_app(app, settings) + + assert "/_mgmt/metrics" not in get_flattened_routes(app) + assert r"^/_mgmt/metrics" not in settings.public_endpoints diff --git a/uv.lock b/uv.lock index ef8baf3..dee2a6b 100644 --- a/uv.lock +++ b/uv.lock @@ -2016,7 +2016,6 @@ dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, { name = "jinja2" }, - { name = "prometheus-fastapi-instrumentator" }, { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "starlette" }, @@ -2038,12 +2037,16 @@ docs = [ lambda = [ { name = "mangum" }, ] +metrics = [ + { name = "prometheus-fastapi-instrumentator" }, +] [package.dev-dependencies] dev = [ { name = "jwcrypto" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "prometheus-fastapi-instrumentator" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -2071,20 +2074,21 @@ requires-dist = [ { name = "mkdocs-api-autonav", marker = "extra == 'docs'", specifier = ">=0.3.0" }, { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'docs'", specifier = ">=9.6.16" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.30.0" }, - { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" }, + { name = "prometheus-fastapi-instrumentator", marker = "extra == 'metrics'", specifier = ">=8.0.2" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "starlette", specifier = ">=1.0.1" }, { name = "starlette-cramjam", specifier = ">=0.4.0" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] -provides-extras = ["docs", "lambda"] +provides-extras = ["docs", "lambda", "metrics"] [package.metadata.requires-dev] dev = [ { name = "jwcrypto", specifier = ">=1.5.6" }, { name = "mypy", specifier = ">=1.3.0" }, { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "pytest-cov", specifier = ">=5.0.0" }, From e1b5efebfe672f1ece7d61c11792bdabe41c4835 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 14 Jul 2026 01:31:46 +0200 Subject: [PATCH 5/8] Exclude probe endpoints. --- src/stac_auth_proxy/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 18cd37c..0e57c1b 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -6,6 +6,7 @@ """ import logging +import re from typing import Any, Optional import httpx @@ -91,7 +92,10 @@ def configure_app( ) if Instrumentator and r"^/_mgmt/metrics" in settings.public_endpoints: - Instrumentator().instrument(app).expose( + excluded_handlers = [r"/_mgmt/"] + if settings.healthz_prefix: + excluded_handlers.append(re.escape(settings.healthz_prefix)) + Instrumentator(excluded_handlers=excluded_handlers).instrument(app).expose( app, endpoint="/_mgmt/metrics", include_in_schema=False ) else: From dca2c8b59453d7b4c866108318822eb8b92777d7 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 14 Jul 2026 01:40:51 +0200 Subject: [PATCH 6/8] Add STAC operation metrics for low-cardinality observability. --- src/stac_auth_proxy/app.py | 7 ++-- src/stac_auth_proxy/metrics.py | 65 ++++++++++++++++++++++++++++++++++ tests/test_configure_app.py | 34 +++++++++++++++++- 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/stac_auth_proxy/metrics.py diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 0e57c1b..08d85c7 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -22,6 +22,7 @@ from .config import Settings from .handlers import HealthzHandler, ReverseProxyHandler, SwaggerUI from .lifespan import build_lifespan +from .metrics import stac_operation_instrumentation from .middleware import ( AddProcessTimeHeaderMiddleware, AuthenticationExtensionMiddleware, @@ -95,9 +96,9 @@ def configure_app( excluded_handlers = [r"/_mgmt/"] if settings.healthz_prefix: excluded_handlers.append(re.escape(settings.healthz_prefix)) - Instrumentator(excluded_handlers=excluded_handlers).instrument(app).expose( - app, endpoint="/_mgmt/metrics", include_in_schema=False - ) + Instrumentator(excluded_handlers=excluded_handlers).instrument(app).add( + stac_operation_instrumentation + ).expose(app, endpoint="/_mgmt/metrics", include_in_schema=False) else: settings.public_endpoints.pop(r"^/_mgmt/metrics", None) diff --git a/src/stac_auth_proxy/metrics.py b/src/stac_auth_proxy/metrics.py new file mode 100644 index 0000000..8ec9769 --- /dev/null +++ b/src/stac_auth_proxy/metrics.py @@ -0,0 +1,65 @@ +"""Optional Prometheus metrics for STAC operations.""" + +import re +from typing import Any, Callable, Optional + +stac_operation_instrumentation: Optional[Callable[[Any], None]] = None + +OPERATIONS = [ # ordered; first match wins + (r"^/$", {"GET": "landing_page"}), + (r"^/conformance$", {"GET": "conformance"}), + (r"^/search$", {"GET": "search", "POST": "search"}), + (r"^/collections$", {"GET": "list_collections", "POST": "create_collection"}), + ( + r"^/collections/[^/]+$", + { + "GET": "get_collection", + "PUT": "edit_collection", + "PATCH": "edit_collection", + "DELETE": "delete_collection", + }, + ), + (r"^/collections/[^/]+/items$", {"GET": "list_items", "POST": "create_item"}), + ( + r"^/collections/[^/]+/items/[^/]+$", + { + "GET": "get_item", + "PUT": "edit_item", + "PATCH": "edit_item", + "DELETE": "delete_item", + }, + ), + (r"^/collections/[^/]+/bulk_items$", {"POST": "bulk_create_items"}), +] +_COMPILED = [(re.compile(p), m) for p, m in OPERATIONS] + + +def classify_operation(method: str, path: str) -> str: + """Map a request to a low-cardinality STAC operation name.""" + for pattern, methods in _COMPILED: + if pattern.match(path): + return methods.get(method.upper(), "other") + return "other" + + +try: + from prometheus_client import Histogram + from prometheus_fastapi_instrumentator.metrics import Info + + _DURATION = Histogram( + "stac_operation_duration_seconds", + "Request duration by STAC operation.", + labelnames=("operation", "status"), + ) + + def _stac_operation_instrumentation(info: Info) -> None: + """Observe request duration labeled by STAC operation.""" + _DURATION.labels( + operation=classify_operation(info.request.method, info.request.url.path), + status=info.modified_status, + ).observe(info.modified_duration) + + stac_operation_instrumentation = _stac_operation_instrumentation + +except ImportError: + pass diff --git a/tests/test_configure_app.py b/tests/test_configure_app.py index 2018e1d..e085ad4 100644 --- a/tests/test_configure_app.py +++ b/tests/test_configure_app.py @@ -1,10 +1,12 @@ """Tests for configuring an external FastAPI application.""" +import pytest from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient import stac_auth_proxy.app as app_module from stac_auth_proxy import Settings, configure_app +from stac_auth_proxy.metrics import classify_operation def get_flattened_routes(app: FastAPI | APIRouter, prefix=""): @@ -80,11 +82,41 @@ def test_metrics_endpoint_returns_prometheus_output(): ) configure_app(app, settings) - response = TestClient(app).get("/_mgmt/metrics") + client = TestClient(app) + app.add_api_route("/collections", lambda: {"collections": []}, methods=["GET"]) + client.get("/collections") + response = client.get("/_mgmt/metrics") assert response.status_code == 200 assert response.headers["content-type"].startswith("text/plain") assert "# HELP" in response.text + assert "stac_operation_duration_seconds" in response.text + + +@pytest.mark.parametrize( + ("method", "path", "expected"), + [ + ("GET", "/", "landing_page"), + ("GET", "/conformance", "conformance"), + ("GET", "/search", "search"), + ("POST", "/search", "search"), + ("GET", "/collections", "list_collections"), + ("POST", "/collections", "create_collection"), + ("GET", "/collections/sentinel-2", "get_collection"), + ("PUT", "/collections/sentinel-2", "edit_collection"), + ("DELETE", "/collections/sentinel-2", "delete_collection"), + ("GET", "/collections/sentinel-2/items", "list_items"), + ("POST", "/collections/sentinel-2/items", "create_item"), + ("GET", "/collections/sentinel-2/items/abc", "get_item"), + ("DELETE", "/collections/sentinel-2/items/abc", "delete_item"), + ("POST", "/collections/sentinel-2/bulk_items", "bulk_create_items"), + ("GET", "/unknown", "other"), + ("POST", "/conformance", "other"), + ], +) +def test_classify_operation(method, path, expected): + """STAC paths map to low-cardinality operation names.""" + assert classify_operation(method, path) == expected def test_metrics_endpoint_skipped_without_instrumentator(monkeypatch): From b78072131c3da2bfae5e760938a1d14b5954644b Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Wed, 22 Jul 2026 11:01:11 +0200 Subject: [PATCH 7/8] Small clean-ups. --- src/stac_auth_proxy/app.py | 19 ++++++++++--------- src/stac_auth_proxy/metrics.py | 5 +++++ tests/test_configure_app.py | 12 ++++++++---- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 08d85c7..1daf2a8 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -12,17 +12,16 @@ import httpx from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware - -try: - from prometheus_fastapi_instrumentator import Instrumentator -except ImportError: - Instrumentator = None from starlette_cramjam.middleware import CompressionMiddleware from .config import Settings from .handlers import HealthzHandler, ReverseProxyHandler, SwaggerUI from .lifespan import build_lifespan -from .metrics import stac_operation_instrumentation +from .metrics import ( + METRICS_AVAILABLE, + Instrumentator, + stac_operation_instrumentation, +) from .middleware import ( AddProcessTimeHeaderMiddleware, AuthenticationExtensionMiddleware, @@ -92,13 +91,15 @@ def configure_app( prefix=settings.healthz_prefix, ) - if Instrumentator and r"^/_mgmt/metrics" in settings.public_endpoints: + if METRICS_AVAILABLE and r"^/_mgmt/metrics" in settings.public_endpoints: excluded_handlers = [r"/_mgmt/"] if settings.healthz_prefix: excluded_handlers.append(re.escape(settings.healthz_prefix)) - Instrumentator(excluded_handlers=excluded_handlers).instrument(app).add( + Instrumentator(excluded_handlers=excluded_handlers).add( stac_operation_instrumentation - ).expose(app, endpoint="/_mgmt/metrics", include_in_schema=False) + ).instrument(app).expose( + app, endpoint="/_mgmt/metrics", include_in_schema=False + ) else: settings.public_endpoints.pop(r"^/_mgmt/metrics", None) diff --git a/src/stac_auth_proxy/metrics.py b/src/stac_auth_proxy/metrics.py index 8ec9769..1e77319 100644 --- a/src/stac_auth_proxy/metrics.py +++ b/src/stac_auth_proxy/metrics.py @@ -3,6 +3,8 @@ import re from typing import Any, Callable, Optional +METRICS_AVAILABLE = False +Instrumentator: Any = None stac_operation_instrumentation: Optional[Callable[[Any], None]] = None OPERATIONS = [ # ordered; first match wins @@ -44,6 +46,7 @@ def classify_operation(method: str, path: str) -> str: try: from prometheus_client import Histogram + from prometheus_fastapi_instrumentator import Instrumentator as _Instrumentator from prometheus_fastapi_instrumentator.metrics import Info _DURATION = Histogram( @@ -59,7 +62,9 @@ def _stac_operation_instrumentation(info: Info) -> None: status=info.modified_status, ).observe(info.modified_duration) + Instrumentator = _Instrumentator stac_operation_instrumentation = _stac_operation_instrumentation + METRICS_AVAILABLE = True except ImportError: pass diff --git a/tests/test_configure_app.py b/tests/test_configure_app.py index e085ad4..72eb0f5 100644 --- a/tests/test_configure_app.py +++ b/tests/test_configure_app.py @@ -79,18 +79,22 @@ def test_metrics_endpoint_returns_prometheus_output(): oidc_discovery_url="https://example.com/.well-known/openid-configuration", wait_for_upstream=False, check_conformance=False, + default_public=True, ) configure_app(app, settings) - client = TestClient(app) app.add_api_route("/collections", lambda: {"collections": []}, methods=["GET"]) - client.get("/collections") + client = TestClient(app) + assert client.get("/collections").status_code == 200 response = client.get("/_mgmt/metrics") assert response.status_code == 200 assert response.headers["content-type"].startswith("text/plain") assert "# HELP" in response.text - assert "stac_operation_duration_seconds" in response.text + assert ( + 'stac_operation_duration_seconds_count{operation="list_collections",status="2xx"}' + in response.text + ) @pytest.mark.parametrize( @@ -121,7 +125,7 @@ def test_classify_operation(method, path, expected): def test_metrics_endpoint_skipped_without_instrumentator(monkeypatch): """Metrics route and public endpoint are omitted when the extra is absent.""" - monkeypatch.setattr(app_module, "Instrumentator", None) + monkeypatch.setattr(app_module, "METRICS_AVAILABLE", False) app = FastAPI() settings = Settings( upstream_url="https://example.com", From 0f1c0684f701da092fa61f7b6d03bb4b6006055d Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Wed, 22 Jul 2026 11:09:29 +0200 Subject: [PATCH 8/8] Align with eoapi-devseed. --- Dockerfile | 4 +- docs/user-guide/getting-started.md | 2 +- src/stac_auth_proxy/app.py | 12 +----- src/stac_auth_proxy/metrics.py | 68 ++++++++++++++++++++---------- tests/test_configure_app.py | 14 +++--- 5 files changed, 60 insertions(+), 40 deletions(-) diff --git a/Dockerfile b/Dockerfile index e96c729..9a3f902 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,13 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --no-dev + uv sync --frozen --no-install-project --no-dev --extra metrics # Then, add the rest of the project source code and install it # Installing separately from its dependencies allows optimal layer caching ADD . /app RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-dev + uv sync --frozen --no-dev --extra metrics # Runtime stage FROM python:3.13-slim diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index ec4d695..252c48d 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -54,7 +54,7 @@ docker run -p 8000:8000 \ pip install stac-auth-proxy ``` > [!TIP] - > To expose a Prometheus `/_mgmt/metrics` endpoint, install the `metrics` extra instead: `pip install stac-auth-proxy[metrics]` + > To expose a Prometheus `/_mgmt/metrics` endpoint, install the `metrics` extra instead: `pip install stac-auth-proxy[metrics]`. 2. Set environment variables: ```bash export UPSTREAM_URL=https://your-stac-api.com/stac diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 1daf2a8..1c56d0c 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -17,11 +17,7 @@ from .config import Settings from .handlers import HealthzHandler, ReverseProxyHandler, SwaggerUI from .lifespan import build_lifespan -from .metrics import ( - METRICS_AVAILABLE, - Instrumentator, - stac_operation_instrumentation, -) +from .metrics import METRICS_AVAILABLE, instrument_app from .middleware import ( AddProcessTimeHeaderMiddleware, AuthenticationExtensionMiddleware, @@ -95,11 +91,7 @@ def configure_app( excluded_handlers = [r"/_mgmt/"] if settings.healthz_prefix: excluded_handlers.append(re.escape(settings.healthz_prefix)) - Instrumentator(excluded_handlers=excluded_handlers).add( - stac_operation_instrumentation - ).instrument(app).expose( - app, endpoint="/_mgmt/metrics", include_in_schema=False - ) + instrument_app(app, excluded_handlers=excluded_handlers) else: settings.public_endpoints.pop(r"^/_mgmt/metrics", None) diff --git a/src/stac_auth_proxy/metrics.py b/src/stac_auth_proxy/metrics.py index 1e77319..ee32d0b 100644 --- a/src/stac_auth_proxy/metrics.py +++ b/src/stac_auth_proxy/metrics.py @@ -1,14 +1,12 @@ """Optional Prometheus metrics for STAC operations.""" import re -from typing import Any, Callable, Optional +from typing import Any, Optional, Sequence METRICS_AVAILABLE = False -Instrumentator: Any = None -stac_operation_instrumentation: Optional[Callable[[Any], None]] = None OPERATIONS = [ # ordered; first match wins - (r"^/$", {"GET": "landing_page"}), + (r"^/$", {"GET": "landing"}), (r"^/conformance$", {"GET": "conformance"}), (r"^/search$", {"GET": "search", "POST": "search"}), (r"^/collections$", {"GET": "list_collections", "POST": "create_collection"}), @@ -31,7 +29,7 @@ "DELETE": "delete_item", }, ), - (r"^/collections/[^/]+/bulk_items$", {"POST": "bulk_create_items"}), + (r"^/collections/[^/]+/bulk_items$", {"POST": "bulk"}), ] _COMPILED = [(re.compile(p), m) for p, m in OPERATIONS] @@ -40,31 +38,57 @@ def classify_operation(method: str, path: str) -> str: """Map a request to a low-cardinality STAC operation name.""" for pattern, methods in _COMPILED: if pattern.match(path): - return methods.get(method.upper(), "other") - return "other" + return methods.get(method.upper(), "unknown") + return "unknown" + + +def instrument_app( + app: Any, + excluded_handlers: Optional[Sequence[str]] = None, +) -> None: + """Instrument a FastAPI app and expose Prometheus metrics when available.""" + if not METRICS_AVAILABLE: + return + _instrument_app(app, list(excluded_handlers or [])) try: - from prometheus_client import Histogram - from prometheus_fastapi_instrumentator import Instrumentator as _Instrumentator + from prometheus_client import Counter, Histogram + from prometheus_fastapi_instrumentator import Instrumentator from prometheus_fastapi_instrumentator.metrics import Info - _DURATION = Histogram( - "stac_operation_duration_seconds", - "Request duration by STAC operation.", - labelnames=("operation", "status"), + REQUESTS = Counter( + "http_requests_total", + "Total HTTP requests by STAC operation.", + labelnames=("operation", "method", "status"), + ) + LATENCY = Histogram( + "http_request_duration_seconds", + "HTTP request latency by STAC operation.", + labelnames=("operation", "method"), + buckets=(0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, float("inf")), ) - def _stac_operation_instrumentation(info: Info) -> None: - """Observe request duration labeled by STAC operation.""" - _DURATION.labels( - operation=classify_operation(info.request.method, info.request.url.path), - status=info.modified_status, - ).observe(info.modified_duration) + def record_stac_metrics(info: Info) -> None: + """Record request count and latency using STAC operation labels.""" + operation = classify_operation(info.request.method, info.request.url.path) + REQUESTS.labels(operation, info.method, info.modified_status).inc() + LATENCY.labels(operation, info.method).observe(info.modified_duration) + + def _instrument_app(app: Any, excluded_handlers: list[str]) -> None: + ( + Instrumentator( + should_group_status_codes=True, + excluded_handlers=excluded_handlers, + ) + .add(record_stac_metrics) + .instrument(app) + .expose(app, endpoint="/_mgmt/metrics", include_in_schema=False) + ) - Instrumentator = _Instrumentator - stac_operation_instrumentation = _stac_operation_instrumentation METRICS_AVAILABLE = True except ImportError: - pass + + def _instrument_app(app: Any, excluded_handlers: list[str]) -> None: + return diff --git a/tests/test_configure_app.py b/tests/test_configure_app.py index 72eb0f5..6a9d819 100644 --- a/tests/test_configure_app.py +++ b/tests/test_configure_app.py @@ -92,7 +92,11 @@ def test_metrics_endpoint_returns_prometheus_output(): assert response.headers["content-type"].startswith("text/plain") assert "# HELP" in response.text assert ( - 'stac_operation_duration_seconds_count{operation="list_collections",status="2xx"}' + 'http_requests_total{method="GET",operation="list_collections",status="2xx"}' + in response.text + ) + assert ( + 'http_request_duration_seconds_count{method="GET",operation="list_collections"}' in response.text ) @@ -100,7 +104,7 @@ def test_metrics_endpoint_returns_prometheus_output(): @pytest.mark.parametrize( ("method", "path", "expected"), [ - ("GET", "/", "landing_page"), + ("GET", "/", "landing"), ("GET", "/conformance", "conformance"), ("GET", "/search", "search"), ("POST", "/search", "search"), @@ -113,9 +117,9 @@ def test_metrics_endpoint_returns_prometheus_output(): ("POST", "/collections/sentinel-2/items", "create_item"), ("GET", "/collections/sentinel-2/items/abc", "get_item"), ("DELETE", "/collections/sentinel-2/items/abc", "delete_item"), - ("POST", "/collections/sentinel-2/bulk_items", "bulk_create_items"), - ("GET", "/unknown", "other"), - ("POST", "/conformance", "other"), + ("POST", "/collections/sentinel-2/bulk_items", "bulk"), + ("GET", "/unknown", "unknown"), + ("POST", "/conformance", "unknown"), ], ) def test_classify_operation(method, path, expected):