Skip to content

Commit afdaa03

Browse files
authored
feat: make the ASGI receive/send spans opt-out on Litestar (#288)
LitestarConfig gets the same opentelemetry_exclude_spans field as FastAPIConfig and passes it to Litestar's OpenTelemetryConfig, which has taken exclude_spans since 2.22, the litestar-otl floor.
1 parent d215273 commit afdaa03

5 files changed

Lines changed: 30 additions & 3 deletions

File tree

‎docs/introduction/configuration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ config = FastAPIConfig(
173173
)
174174
```
175175

176-
For FastAPI there is additionally:
176+
For FastAPI and Litestar there is additionally:
177177

178178
- `opentelemetry_exclude_spans` - drops the ASGI `receive` and/or `send` spans, leaving only the
179179
server span. Empty by default, which records all three. `["receive", "send"]` is worth ~34 µs per

‎docs/introduction/performance.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ In rough order of what they return:
4646
|---|---|---|
4747
| [`opentelemetry_sampler`](configuration.md#opentelemetry), 1% ratio | ~55 µs/req | 99% of your traces |
4848
| `http_methods_to_capture=()` on the Sentry ASGI integrations | ~35 µs/req | Sentry-side trace correlation |
49-
| [`opentelemetry_exclude_spans`](configuration.md#opentelemetry), FastAPI only | ~34 µs/req | the two ASGI event spans |
49+
| [`opentelemetry_exclude_spans`](configuration.md#opentelemetry), FastAPI and Litestar | ~34 µs/req | the two ASGI event spans |
5050
| [`sentry_auto_session_tracking=False`](configuration.md#sentry) | ~7.7 µs/req | Sentry release health |
5151
| [`sentry_logging_breadcrumb_level=None`](configuration.md#sentry-logging-integration) | ~7 µs per log record | log breadcrumbs on errors |
5252

‎lite_bootstrap/bootstrappers/fastapi_bootstrapper.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class FastAPIConfig(
6161
):
6262
application: "fastapi.FastAPI | UnsetType" = UNSET
6363
application_kwargs: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
64-
# Not on OpenTelemetryConfig: `exclude_spans` is this instrumentor's parameter, and Litestar's has none.
64+
# Not on OpenTelemetryConfig: only the FastAPI and Litestar instrumentors take `exclude_spans`.
6565
opentelemetry_exclude_spans: list[typing.Literal["receive", "send"]] = dataclasses.field(default_factory=list)
6666
prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
6767
prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)

‎lite_bootstrap/bootstrappers/litestar_bootstrapper.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class LitestarConfig(
123123
application_config: "AppConfig" = dataclasses.field(default_factory=lambda: AppConfig()) # noqa: PLW0108
124124
litestar_logging_middleware_config: "LoggingMiddlewareConfig | None" = None
125125
litestar_logging_middleware_enabled: bool = False
126+
opentelemetry_exclude_spans: list[typing.Literal["receive", "send"]] = dataclasses.field(default_factory=list)
126127
prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
127128
# Bounds path-label cardinality (Litestar defaults False -> raw URLs leak memory). See litestar#4891.
128129
prometheus_group_path: bool = True
@@ -237,6 +238,7 @@ def bootstrap(self) -> None:
237238
scope_span_details_extractor=build_litestar_route_details_from_scope,
238239
# An empty list compiles to a pattern that matches, and so skips, every path
239240
exclude=self._build_excluded_path_patterns() or None,
241+
exclude_spans=self.bootstrap_config.opentelemetry_exclude_spans,
240242
)
241243
)
242244
)

‎tests/test_litestar_bootstrap.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,31 @@ async def get_user(user_id: int) -> dict[str, int]:
605605
assert server_spans[0].attributes["http.route"] == "/users/{user_id}"
606606

607607

608+
def _span_kinds_for_one_request(config: LitestarConfig) -> list[SpanKind]:
609+
@litestar.get("/items")
610+
async def get_items() -> list[int]:
611+
return [1]
612+
613+
config = dataclasses.replace(config, application_config=AppConfig(route_handlers=[get_items]))
614+
application, exporter = _bootstrap_with_span_exporter(config)
615+
616+
with TestClient(app=application) as client:
617+
assert client.get("/items").status_code == status_codes.HTTP_200_OK
618+
619+
return sorted((span.kind for span in exporter.get_finished_spans()), key=lambda kind: kind.value)
620+
621+
622+
def test_litestar_otel_exclude_spans_defaults_to_recording_them(litestar_config: LitestarConfig) -> None:
623+
"""The default keeps the ASGI event spans a trace view shows today; dropping them is opt-in."""
624+
assert SpanKind.INTERNAL in _span_kinds_for_one_request(litestar_config)
625+
626+
627+
def test_litestar_otel_exclude_spans_leaves_only_the_server_span(litestar_config: LitestarConfig) -> None:
628+
config = dataclasses.replace(litestar_config, opentelemetry_exclude_spans=["receive", "send"])
629+
630+
assert _span_kinds_for_one_request(config) == [SpanKind.SERVER]
631+
632+
608633
def test_litestar_otel_unmatched_path_span_is_named_by_method_only(litestar_config: LitestarConfig) -> None:
609634
"""A 404 never reaches the route stack, so its span must not carry the raw path that scanners vary."""
610635
application, exporter = _bootstrap_with_span_exporter(litestar_config)

0 commit comments

Comments
 (0)