diff --git a/AGENTS.md b/AGENTS.md index 27864b1..18be7e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Python is 3.14, dependencies managed by `uv`. The API is exposed on `:8000`. **Stack**: Litestar + SQLAlchemy 2 (async) + advanced-alchemy + Alembic + Postgres + Granian (ASGI server) + modern-di (IoC) + lite-bootstrap (observability/CORS/Sentry/OTel wiring). -**Request flow**: `app/__main__.py` → `granian` → `app.application:build_app` (factory) → `LitestarBootstrapper` from `lite-bootstrap` wraps a `litestar.Litestar` with OpenTelemetry (asyncpg + SQLAlchemy instrumentors), Sentry, CORS, Swagger, etc., based on `Settings.api_bootstrapper_config`. +**Request flow**: `app/__main__.py` → `granian` → `app.application:build_app` (factory) → `LitestarBootstrapper` from `lite-bootstrap` wraps a `litestar.Litestar` with OpenTelemetry (SQLAlchemy instrumentor), Sentry, CORS, Swagger, etc., based on `Settings.api_bootstrapper_config`. **Dependency injection** (`app/ioc.py`): `modern_di.Container` is created in `build_app` with the `Dependencies` group and attached via `modern_di_litestar.ModernDIPlugin`. Route handlers receive repositories as parameters; `application.py` declares them with `modern_di_litestar.FromDI(...)` so Litestar resolves them per-request. Provider scopes: - `database_engine` — application-scoped primary engine, finalizer disposes the engine diff --git a/app/application.py b/app/application.py index ac8101a..a216a9e 100644 --- a/app/application.py +++ b/app/application.py @@ -6,7 +6,6 @@ from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError from lite_bootstrap import LitestarBootstrapper from litestar.config.app import AppConfig -from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from app import exceptions, ioc, repositories @@ -35,10 +34,7 @@ def build_app() -> litestar.Litestar: }, request_max_body_size=settings.request_max_body_size, ), - opentelemetry_instrumentors=[ - SQLAlchemyInstrumentor(), - AsyncPGInstrumentor(capture_parameters=True), - ], + opentelemetry_instrumentors=[SQLAlchemyInstrumentor()], ) bootstrapper = LitestarBootstrapper(bootstrap_config=bootstrap_config) return bootstrapper.bootstrap() diff --git a/pyproject.toml b/pyproject.toml index 98d5939..b10d004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ "sqlalchemy[asyncio]<2.1", "asyncpg", # tracing - "opentelemetry-instrumentation-asyncpg", "opentelemetry-instrumentation-sqlalchemy", ] diff --git a/tests/test_tracing.py b/tests/test_tracing.py new file mode 100644 index 0000000..5cfa4fd --- /dev/null +++ b/tests/test_tracing.py @@ -0,0 +1,40 @@ +import dataclasses + +import pytest +from asgi_lifespan import LifespanManager +from httpx import ASGITransport, AsyncClient +from litestar import status_codes +from opentelemetry import trace +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 app.application import build_app +from app.settings import Settings, settings + + +async def test_db_queries_produce_sqlalchemy_spans(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() + app = build_app() + tracer_provider = trace.get_tracer_provider() + assert isinstance(tracer_provider, TracerProvider) + tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) + + async with ( + LifespanManager(app), # ty: ignore[invalid-argument-type] + AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client, # ty: ignore[invalid-argument-type] + ): + response = await client.get("/api/decks/") + + assert response.status_code == status_codes.HTTP_200_OK + db_spans = [ + span + for span in exporter.get_finished_spans() + if span.instrumentation_scope is not None + and span.instrumentation_scope.name == "opentelemetry.instrumentation.sqlalchemy" + and span.name.startswith("SELECT") + ] + assert db_spans + assert all(span.parent is not None for span in db_spans) diff --git a/uv.lock b/uv.lock index 016d04c..94494a1 100644 --- a/uv.lock +++ b/uv.lock @@ -491,7 +491,6 @@ dependencies = [ { name = "lite-bootstrap", extra = ["litestar-all"] }, { name = "litestar" }, { name = "modern-di-litestar" }, - { name = "opentelemetry-instrumentation-asyncpg" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "psycopg" }, { name = "pydantic-settings" }, @@ -522,7 +521,6 @@ requires-dist = [ { name = "lite-bootstrap", extras = ["litestar-all"] }, { name = "litestar" }, { name = "modern-di-litestar", specifier = ">=4,<5" }, - { name = "opentelemetry-instrumentation-asyncpg" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "psycopg" }, { name = "pydantic-settings" }, @@ -830,20 +828,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, ] -[[package]] -name = "opentelemetry-instrumentation-asyncpg" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/03/de/21b57f84a41385d7055547c03b491b0de0531b0fa5a03cd4a6499a57a8f8/opentelemetry_instrumentation_asyncpg-0.65b0.tar.gz", hash = "sha256:0b14338886f17c18a9899d6764117a4c163a9826f6ed1577f68ee773f95ebbc2", size = 11202, upload-time = "2026-07-16T15:25:57.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/18/c021ac484e447fd185a968cf3b134b67c30bfbda6f0add347da1d7c109e2/opentelemetry_instrumentation_asyncpg-0.65b0-py3-none-any.whl", hash = "sha256:9156d2501021b968da0f317eb508d3efb3e7506bf50e00ee477ce0636a12772a", size = 9744, upload-time = "2026-07-16T15:25:00.172Z" }, -] - [[package]] name = "opentelemetry-instrumentation-sqlalchemy" version = "0.65b0"