@@ -549,7 +549,65 @@ def test_litestar_otel_middleware_hands_the_instrumentor_a_parsed_exclude_list()
549549
550550 excluded_urls = middleware ._excluded_urls # noqa: SLF001
551551 assert not isinstance (excluded_urls , str )
552- # Matched against the URL the middleware builds from `scope["path"]`, which Litestar has
553- # already normalized; see #248 for the trailing-slash entries that therefore never match.
554552 assert excluded_urls .url_disabled ("http://test/custom-metrics" )
555553 assert not excluded_urls .url_disabled ("http://test/items/1" )
554+
555+
556+ def test_litestar_otel_excludes_infrastructure_paths_normalized_by_litestar (
557+ litestar_config : LitestarConfig ,
558+ ) -> None :
559+ """REGRESSION #248: Litestar strips the trailing slash before the middleware sees the path.
560+
561+ `health_checks_path="/custom-health/"` reaches `OpenTelemetryMiddleware` as
562+ `http://host/custom-health`, so an exclude entry carrying the slash never matched and the
563+ health check was traced anyway. The lookalike route guards the obvious over-correction:
564+ `ExcludeList` regex-searches unanchored, so a bare `/custom-health` prefix would also
565+ silence `/custom-healthy`.
566+ """
567+
568+ @litestar .get ("/custom-healthy" )
569+ async def lookalike_handler () -> dict [str , str ]:
570+ return {"status" : "ok" }
571+
572+ config = dataclasses .replace (litestar_config , application_config = AppConfig (route_handlers = [lookalike_handler ]))
573+ application = LitestarBootstrapper (bootstrap_config = config ).bootstrap ()
574+
575+ tracer_provider = get_tracer_provider ()
576+ assert isinstance (tracer_provider , SDKTracerProvider )
577+ exporter = InMemorySpanExporter ()
578+ tracer_provider .add_span_processor (SimpleSpanProcessor (exporter ))
579+
580+ with TestClient (app = application ) as client :
581+ assert client .get (config .health_checks_path ).status_code == status_codes .HTTP_200_OK
582+ assert client .get (config .prometheus_metrics_path ).status_code == status_codes .HTTP_200_OK
583+ assert client .get ("/custom-healthy" ).status_code == status_codes .HTTP_200_OK
584+
585+ span_names = [span .name for span in exporter .get_finished_spans ()]
586+ assert "GET /custom-health" not in span_names
587+ assert "GET /custom-metrics" not in span_names
588+ assert "GET /custom-healthy" in span_names
589+
590+
591+ def test_litestar_otel_keeps_caller_supplied_excluded_urls_as_regexes (litestar_config : LitestarConfig ) -> None :
592+ """`opentelemetry_excluded_urls` entries are OpenTelemetry regexes, so anchoring must not touch them."""
593+
594+ @litestar .get ("/items/{item_id:int}" )
595+ async def get_item (item_id : int ) -> dict [str , int ]:
596+ return {"item_id" : item_id }
597+
598+ config = dataclasses .replace (
599+ litestar_config ,
600+ application_config = AppConfig (route_handlers = [get_item ]),
601+ opentelemetry_excluded_urls = [r"/items/\d+$" ],
602+ )
603+ application = LitestarBootstrapper (bootstrap_config = config ).bootstrap ()
604+
605+ tracer_provider = get_tracer_provider ()
606+ assert isinstance (tracer_provider , SDKTracerProvider )
607+ exporter = InMemorySpanExporter ()
608+ tracer_provider .add_span_processor (SimpleSpanProcessor (exporter ))
609+
610+ with TestClient (app = application ) as client :
611+ assert client .get ("/items/42" ).status_code == status_codes .HTTP_200_OK
612+
613+ assert exporter .get_finished_spans () == ()
0 commit comments