diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index 0e35807..e562731 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -95,6 +95,13 @@ config = FastAPIConfig( ) ``` +For FastAPI there is additionally: + +- `opentelemetry_exclude_spans` - drops the ASGI `receive` and/or `send` spans, leaving only the + server span. Empty by default, which records all three. `["receive", "send"]` is worth ~33 µs per + request on the benchmark endpoint, at the cost of two thirds of the spans disappearing from your + trace view. + For FastStream you must provide additionally: - `opentelemetry_middleware_cls` diff --git a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py index 78c6ca3..f4a27db 100644 --- a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py @@ -50,6 +50,8 @@ class FastAPIConfig( ): application: "fastapi.FastAPI | UnsetType" = UNSET application_kwargs: dict[str, typing.Any] = dataclasses.field(default_factory=dict) + # Not on OpenTelemetryConfig: `exclude_spans` is this instrumentor's parameter, and Litestar's has none. + opentelemetry_exclude_spans: list[typing.Literal["receive", "send"]] = dataclasses.field(default_factory=list) prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) prometheus_expose_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) @@ -119,6 +121,7 @@ def bootstrap(self) -> None: app=self.bootstrap_config.app, tracer_provider=get_tracer_provider(), excluded_urls=",".join(self._build_excluded_urls()), + exclude_spans=self.bootstrap_config.opentelemetry_exclude_spans, ) def teardown(self) -> None: diff --git a/tests/test_fastapi_bootstrap.py b/tests/test_fastapi_bootstrap.py index 7dd78ed..cf4c7e9 100644 --- a/tests/test_fastapi_bootstrap.py +++ b/tests/test_fastapi_bootstrap.py @@ -1,5 +1,6 @@ import dataclasses import logging +import typing import warnings from unittest.mock import patch @@ -10,6 +11,7 @@ from starlette.testclient import TestClient from lite_bootstrap import FastAPIBootstrapper, FastAPIConfig, import_checker +from lite_bootstrap.bootstrappers import fastapi_bootstrapper from lite_bootstrap.exceptions import ConfigurationError, InstrumentDependencyMissingWarning from lite_bootstrap.types import UNSET from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing, warning_source_files @@ -39,6 +41,28 @@ def fastapi_config() -> FastAPIConfig: ) +def _instrument_app_kwargs(config: FastAPIConfig) -> dict[str, typing.Any]: + """Bootstrap with FastAPIInstrumentor stubbed, and return what it was handed.""" + with patch.object(fastapi_bootstrapper, "FastAPIInstrumentor") as mock_instrumentor: + bootstrapper = FastAPIBootstrapper(bootstrap_config=config) + bootstrapper.bootstrap() + try: + return mock_instrumentor.instrument_app.call_args.kwargs + finally: + bootstrapper.teardown() + + +def test_fastapi_opentelemetry_exclude_spans_defaults_to_recording_them(fastapi_config: FastAPIConfig) -> None: + """The default keeps every span a trace view shows today; dropping two of three is opt-in.""" + assert _instrument_app_kwargs(fastapi_config)["exclude_spans"] == [] + + +def test_fastapi_opentelemetry_exclude_spans_reaches_the_instrumentor(fastapi_config: FastAPIConfig) -> None: + config = dataclasses.replace(fastapi_config, opentelemetry_exclude_spans=["receive", "send"]) + + assert _instrument_app_kwargs(config)["exclude_spans"] == ["receive", "send"] + + def test_fastapi_bootstrap(fastapi_config: FastAPIConfig) -> None: bootstrapper = FastAPIBootstrapper(bootstrap_config=fastapi_config) application = bootstrapper.bootstrap()