From b23b8e42f0ab9084d0912b7b4d21150003e35c33 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 16 Oct 2026 14:09:56 +0300 Subject: [PATCH 1/2] chore: upgrade lite-bootstrap to 1.13.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b10d004..842a9f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ license = "MIT" dependencies = [ "litestar", - "lite-bootstrap[litestar-all]", + "lite-bootstrap[litestar-all]>=1.13", "modern-di-litestar>=4,<5", "advanced-alchemy", "pydantic-settings", diff --git a/uv.lock b/uv.lock index 94494a1..117a941 100644 --- a/uv.lock +++ b/uv.lock @@ -421,14 +421,14 @@ wheels = [ [[package]] name = "lite-bootstrap" -version = "1.9.2" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/fd/5313200256e345fb2ae81719342331144949df64b33188610105532ae319/lite_bootstrap-1.9.2.tar.gz", hash = "sha256:5f3d1a0a2b49f5426777f268034980eca84f5cb52015a971017104baa7ec83ba", size = 1838404, upload-time = "2026-09-20T17:35:17.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/71/645aaf63a35ef09615c03d22958ebe7244bbc5349a5e3d3c2e13886def7a/lite_bootstrap-1.13.0.tar.gz", hash = "sha256:ec5e0897fa1855f4911d0b61467b2da954500d3a8ee977eb7e71bc006090d921", size = 1843890, upload-time = "2026-10-16T11:07:05.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/01/4b8bfd0818e5e799b7ffd3947e3cae70657b5cfb06d6c585e91b8e2cfd92/lite_bootstrap-1.9.2-py3-none-any.whl", hash = "sha256:35381b70b0e070b7abcc24e820d289899d9aadd8bee5ce8ce747402307633849", size = 1850273, upload-time = "2026-09-20T17:35:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/ab/8e/1a53f12b0a18a55c98cb54cefef7e1520eadb26ee0261ce6295a8041546a/lite_bootstrap-1.13.0-py3-none-any.whl", hash = "sha256:36ae7d76fed4d5c0b782e199367ab47e4d22ff2d87a578fa84112a9ded89fa22", size = 1853736, upload-time = "2026-10-16T11:07:03.922Z" }, ] [package.optional-dependencies] @@ -518,7 +518,7 @@ requires-dist = [ { name = "alembic" }, { name = "asyncpg" }, { name = "granian", extras = ["uvloop"] }, - { name = "lite-bootstrap", extras = ["litestar-all"] }, + { name = "lite-bootstrap", extras = ["litestar-all"], specifier = ">=1.13" }, { name = "litestar" }, { name = "modern-di-litestar", specifier = ">=4,<5" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, From 361303e88e2945d8fa80455dd176a63fd4f53111 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 16 Oct 2026 14:11:53 +0300 Subject: [PATCH 2/2] feat: drop the ASGI send/receive spans from traces Sets opentelemetry_exclude_spans=["receive", "send"], so a traced request records its server span and database spans only. --- app/settings.py | 1 + readme.md | 2 ++ tests/test_tracing.py | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/settings.py b/app/settings.py index 54ee2c0..7c290e4 100644 --- a/app/settings.py +++ b/app/settings.py @@ -43,6 +43,7 @@ def api_bootstrapper_config(self) -> LitestarConfig: service_environment=self.service_environment, service_debug=self.service_debug, opentelemetry_endpoint=self.opentelemetry_endpoint, + opentelemetry_exclude_spans=["receive", "send"], sentry_dsn=self.sentry_dsn, cors_allowed_origins=self.cors_allowed_origins, cors_allowed_methods=self.cors_allowed_methods, diff --git a/readme.md b/readme.md index 7e735e0..534c61d 100644 --- a/readme.md +++ b/readme.md @@ -41,6 +41,8 @@ OTEL_TRACES_SAMPLER_ARG=0.1 This keeps one trace in ten, and follows the caller's decision when a request arrives with a trace context. Errors still reach Sentry when `SENTRY_DSN` is set. +The template also turns off the ASGI `send` and `receive` spans (`opentelemetry_exclude_spans` in `app/settings.py`), so each request records its server span and database spans only. With every request traced, that raised throughput by 6 to 7% in the same benchmark. Remove the setting if you need those spans. + ## 📝 [License](https://github.com/modern-python/litestar-sqlalchemy-template/blob/main/LICENSE) ## Part of `modern-python` diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 5cfa4fd..f46c808 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -8,12 +8,13 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import SpanKind from app.application import build_app from app.settings import Settings, settings -async def test_db_queries_produce_sqlalchemy_spans(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_traced_request_records_server_and_db_spans_only(monkeypatch: pytest.MonkeyPatch) -> None: traced_config = dataclasses.replace(settings.api_bootstrapper_config, opentelemetry_log_traces=True) monkeypatch.setattr(Settings, "api_bootstrapper_config", traced_config) exporter = InMemorySpanExporter() @@ -29,12 +30,20 @@ async def test_db_queries_produce_sqlalchemy_spans(monkeypatch: pytest.MonkeyPat response = await client.get("/api/decks/") assert response.status_code == status_codes.HTTP_200_OK + spans = exporter.get_finished_spans() + sqlalchemy_scope = "opentelemetry.instrumentation.sqlalchemy" db_spans = [ span - for span in exporter.get_finished_spans() + for span in spans if span.instrumentation_scope is not None - and span.instrumentation_scope.name == "opentelemetry.instrumentation.sqlalchemy" + and span.instrumentation_scope.name == sqlalchemy_scope and span.name.startswith("SELECT") ] assert db_spans assert all(span.parent is not None for span in db_spans) + request_spans = [ + span + for span in spans + if span.instrumentation_scope is None or span.instrumentation_scope.name != sqlalchemy_scope + ] + assert [span.kind for span in request_spans] == [SpanKind.SERVER]