diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index e8cefff..a846c53 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -52,6 +52,7 @@ from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.trace import TracerProvider + from opentelemetry.util.http import parse_excluded_urls if import_checker.is_opentelemetry_installed: from opentelemetry.trace import get_tracer_provider @@ -99,7 +100,8 @@ def build_litestar_route_details_from_scope( class LitestarOpenTelemetryInstrumentationMiddleware(ASGIMiddleware): def __init__(self, tracer_provider: "TracerProvider", excluded_urls: set[str]) -> None: self._tracer_provider = tracer_provider - self._excluded_urls = ",".join(excluded_urls) + # OpenTelemetryMiddleware only parses a raw string from 0.56b0; the floor is 0.49b0. + self._excluded_urls = parse_excluded_urls(",".join(excluded_urls)) # WeakKeyDictionary so wrapper apps are evicted when Litestar drops the # next_app reference (hot reload, plugin add/remove, AppConfig rebuild). # Apps that don't support weak references are simply not cached. diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index 42a73b1..17689a5 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -532,3 +532,24 @@ def test_second_litestar_bootstrapper_bootstrap_raises(litestar_config: Litestar assert client.get(litestar_config.health_checks_path).status_code == status_codes.HTTP_200_OK finally: first.teardown() + + +def test_litestar_otel_middleware_hands_the_instrumentor_a_parsed_exclude_list() -> None: + """INVARIANT: `excluded_urls` reaches OpenTelemetryMiddleware parsed, never as a raw string. + + `OpenTelemetryMiddleware` only learned to parse a string itself in + opentelemetry-instrumentation 0.56b0. The declared floor is 0.49b0, where a string reaches + `self.excluded_urls.url_disabled(url)` and raises `AttributeError` on every request, so a + Litestar service with OpenTelemetry returns 500 for everything. + """ + middleware = LitestarOpenTelemetryInstrumentationMiddleware( + tracer_provider=TracerProvider(), + excluded_urls={"/custom-metrics"}, + ) + + excluded_urls = middleware._excluded_urls # noqa: SLF001 + assert not isinstance(excluded_urls, str) + # Matched against the URL the middleware builds from `scope["path"]`, which Litestar has + # already normalized; see #248 for the trailing-slash entries that therefore never match. + assert excluded_urls.url_disabled("http://test/custom-metrics") + assert not excluded_urls.url_disabled("http://test/items/1")