Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ config = FastAPIConfig(
)
```

For FastAPI there is additionally:
For FastAPI and Litestar 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 ~34 µs per
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ In rough order of what they return:
|---|---|---|
| [`opentelemetry_sampler`](configuration.md#opentelemetry), 1% ratio | ~55 µs/req | 99% of your traces |
| `http_methods_to_capture=()` on the Sentry ASGI integrations | ~35 µs/req | Sentry-side trace correlation |
| [`opentelemetry_exclude_spans`](configuration.md#opentelemetry), FastAPI only | ~34 µs/req | the two ASGI event spans |
| [`opentelemetry_exclude_spans`](configuration.md#opentelemetry), FastAPI and Litestar | ~34 µs/req | the two ASGI event spans |
| [`sentry_auto_session_tracking=False`](configuration.md#sentry) | ~7.7 µs/req | Sentry release health |
| [`sentry_logging_breadcrumb_level=None`](configuration.md#sentry-logging-integration) | ~7 µs per log record | log breadcrumbs on errors |

Expand Down
2 changes: 1 addition & 1 deletion lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ 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.
# Not on OpenTelemetryConfig: only the FastAPI and Litestar instrumentors take `exclude_spans`.
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)
Expand Down
2 changes: 2 additions & 0 deletions lite_bootstrap/bootstrappers/litestar_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class LitestarConfig(
application_config: "AppConfig" = dataclasses.field(default_factory=lambda: AppConfig()) # noqa: PLW0108
litestar_logging_middleware_config: "LoggingMiddlewareConfig | None" = None
litestar_logging_middleware_enabled: bool = False
opentelemetry_exclude_spans: list[typing.Literal["receive", "send"]] = dataclasses.field(default_factory=list)
prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
# Bounds path-label cardinality (Litestar defaults False -> raw URLs leak memory). See litestar#4891.
prometheus_group_path: bool = True
Expand Down Expand Up @@ -237,6 +238,7 @@ def bootstrap(self) -> None:
scope_span_details_extractor=build_litestar_route_details_from_scope,
# An empty list compiles to a pattern that matches, and so skips, every path
exclude=self._build_excluded_path_patterns() or None,
exclude_spans=self.bootstrap_config.opentelemetry_exclude_spans,
)
)
)
Expand Down
25 changes: 25 additions & 0 deletions tests/test_litestar_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,31 @@ async def get_user(user_id: int) -> dict[str, int]:
assert server_spans[0].attributes["http.route"] == "/users/{user_id}"


def _span_kinds_for_one_request(config: LitestarConfig) -> list[SpanKind]:
@litestar.get("/items")
async def get_items() -> list[int]:
return [1]

config = dataclasses.replace(config, application_config=AppConfig(route_handlers=[get_items]))
application, exporter = _bootstrap_with_span_exporter(config)

with TestClient(app=application) as client:
assert client.get("/items").status_code == status_codes.HTTP_200_OK

return sorted((span.kind for span in exporter.get_finished_spans()), key=lambda kind: kind.value)


def test_litestar_otel_exclude_spans_defaults_to_recording_them(litestar_config: LitestarConfig) -> None:
"""The default keeps the ASGI event spans a trace view shows today; dropping them is opt-in."""
assert SpanKind.INTERNAL in _span_kinds_for_one_request(litestar_config)


def test_litestar_otel_exclude_spans_leaves_only_the_server_span(litestar_config: LitestarConfig) -> None:
config = dataclasses.replace(litestar_config, opentelemetry_exclude_spans=["receive", "send"])

assert _span_kinds_for_one_request(config) == [SpanKind.SERVER]


def test_litestar_otel_unmatched_path_span_is_named_by_method_only(litestar_config: LitestarConfig) -> None:
"""A 404 never reaches the route stack, so its span must not carry the raw path that scanners vary."""
application, exporter = _bootstrap_with_span_exporter(litestar_config)
Expand Down
Loading