From 1e255f3965f4096a1750cf524c81a46b04149d2f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 20 Sep 2026 17:38:03 +0300 Subject: [PATCH 1/2] fix: parse excluded_urls before handing them to the ASGI instrumentor Closes #247 --- .../bootstrappers/litestar_bootstrapper.py | 5 ++++- tests/test_litestar_bootstrap.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index e8cefff..21174fe 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,9 @@ 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) + # Parsed, not joined: OpenTelemetryMiddleware only accepts a raw string from + # opentelemetry-instrumentation 0.56b0, and the declared 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..6893ade 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -532,3 +532,22 @@ 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-health/", "/custom-metrics/"}, + ) + + excluded_urls = middleware._excluded_urls # noqa: SLF001 + assert not isinstance(excluded_urls, str) + assert excluded_urls.url_disabled("http://test/custom-health/") + assert not excluded_urls.url_disabled("http://test/items/1") From 77064688e515d89ed84839ca2f9fa32e29b21b24 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 20 Sep 2026 17:46:44 +0300 Subject: [PATCH 2/2] review: stop the test asserting a URL the middleware never sees --- lite_bootstrap/bootstrappers/litestar_bootstrapper.py | 3 +-- tests/test_litestar_bootstrap.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index 21174fe..a846c53 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -100,8 +100,7 @@ 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 - # Parsed, not joined: OpenTelemetryMiddleware only accepts a raw string from - # opentelemetry-instrumentation 0.56b0, and the declared floor is 0.49b0. + # 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). diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index 6893ade..17689a5 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -544,10 +544,12 @@ def test_litestar_otel_middleware_hands_the_instrumentor_a_parsed_exclude_list() """ middleware = LitestarOpenTelemetryInstrumentationMiddleware( tracer_provider=TracerProvider(), - excluded_urls={"/custom-health/", "/custom-metrics/"}, + excluded_urls={"/custom-metrics"}, ) excluded_urls = middleware._excluded_urls # noqa: SLF001 assert not isinstance(excluded_urls, str) - assert excluded_urls.url_disabled("http://test/custom-health/") + # 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")