|
13 | 13 | import structlog |
14 | 14 | from opentelemetry.metrics import get_meter_provider |
15 | 15 | from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider |
| 16 | +from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider |
| 17 | +from opentelemetry.sdk.trace.export import SimpleSpanProcessor |
| 18 | +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter |
| 19 | +from opentelemetry.trace import SpanKind, get_tracer_provider |
16 | 20 | from starlette import status |
17 | 21 | from starlette.testclient import TestClient |
18 | 22 |
|
@@ -442,3 +446,58 @@ def test_fastapi_otel_hands_the_instrumentor_the_meter_provider(fastapi_config: |
442 | 446 |
|
443 | 447 | assert mock_instrument_app.call_args.kwargs["meter_provider"] is get_meter_provider() |
444 | 448 | assert isinstance(get_meter_provider(), SDKMeterProvider) |
| 449 | + |
| 450 | + |
| 451 | +def _server_span_names_for(config: FastAPIConfig, paths: typing.Iterable[str]) -> list[str]: |
| 452 | + application = config.app |
| 453 | + |
| 454 | + @application.get("/api/custom-metrics/report") |
| 455 | + async def metrics_report() -> str: |
| 456 | + return "ok" |
| 457 | + |
| 458 | + @application.get("/custom-healthy") |
| 459 | + async def custom_healthy() -> str: |
| 460 | + return "ok" |
| 461 | + |
| 462 | + @application.get("/from-env") |
| 463 | + async def from_env() -> str: |
| 464 | + return "ok" |
| 465 | + |
| 466 | + bootstrapper = FastAPIBootstrapper(bootstrap_config=config) |
| 467 | + bootstrapped_application = bootstrapper.bootstrap() |
| 468 | + tracer_provider = get_tracer_provider() |
| 469 | + assert isinstance(tracer_provider, SDKTracerProvider) |
| 470 | + exporter = InMemorySpanExporter() |
| 471 | + tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) |
| 472 | + try: |
| 473 | + with TestClient(bootstrapped_application) as client: |
| 474 | + for path in paths: |
| 475 | + client.get(path, follow_redirects=False) |
| 476 | + finally: |
| 477 | + bootstrapper.teardown() |
| 478 | + return [span.name for span in exporter.get_finished_spans() if span.kind == SpanKind.SERVER] |
| 479 | + |
| 480 | + |
| 481 | +def test_fastapi_otel_excludes_derived_paths_only_where_they_match_whole_segments( |
| 482 | + fastapi_config: FastAPIConfig, |
| 483 | +) -> None: |
| 484 | + """REGRESSION: `/custom-metrics/` was searched anywhere in the URL, silencing `/api/custom-metrics/report`.""" |
| 485 | + span_names = _server_span_names_for( |
| 486 | + fastapi_config, |
| 487 | + ["/custom-metrics/", "/custom-health/", "/api/custom-metrics/report", "/custom-healthy"], |
| 488 | + ) |
| 489 | + |
| 490 | + assert sorted(span_names) == ["GET /api/custom-metrics/report", "GET /custom-healthy"] |
| 491 | + |
| 492 | + |
| 493 | +@pytest.mark.parametrize("environment_variable", ["OTEL_PYTHON_FASTAPI_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS"]) |
| 494 | +def test_fastapi_otel_honors_excluded_urls_from_environment( |
| 495 | + fastapi_config: FastAPIConfig, monkeypatch: pytest.MonkeyPatch, environment_variable: str |
| 496 | +) -> None: |
| 497 | + for one_variable in ("OTEL_PYTHON_FASTAPI_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS"): |
| 498 | + monkeypatch.delenv(one_variable, raising=False) |
| 499 | + monkeypatch.setenv(environment_variable, "/from-env") |
| 500 | + |
| 501 | + span_names = _server_span_names_for(fastapi_config, ["/from-env", "/custom-healthy"]) |
| 502 | + |
| 503 | + assert span_names == ["GET /custom-healthy"] |
0 commit comments