From 8ec64fa1fe4289a1910baaf06d0adea232e1bc55 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 15 Oct 2026 15:26:06 +0300 Subject: [PATCH] chore: drop the asyncpg OpenTelemetry instrumentor Keep SQLAlchemyInstrumentor as the only database instrumentor and add a test that DB queries still produce SQLAlchemy spans. --- AGENTS.md | 2 +- app/application.py | 6 +----- pyproject.toml | 1 - tests/test_tracing.py | 41 +++++++++++++++++++++++++++++++++++++++++ uv.lock | 16 ---------------- 5 files changed, 43 insertions(+), 23 deletions(-) create mode 100644 tests/test_tracing.py diff --git a/AGENTS.md b/AGENTS.md index c2bb35d..b1b0ee2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ CI (`.github/workflows/main.yml`) runs `ruff format --check`, `ruff check --no-f `app/__main__.py` boots Granian pointing at `app.application:build_app` (factory). `build_app()` in `app/application.py`: 1. Creates a `modern_di.Container` with the `Dependencies` group from `app/ioc.py`. -2. Builds a `FastAPIBootstrapper` from `settings.api_bootstrapper_config`, injecting SQLAlchemy + asyncpg OpenTelemetry instrumentors. +2. Builds a `FastAPIBootstrapper` from `settings.api_bootstrapper_config`, injecting the SQLAlchemy OpenTelemetry instrumentor. 3. `modern_di_fastapi.setup_di(app, container)` wires DI scopes onto the FastAPI app. 4. Includes one `ROUTER` per resource (`app.api.decks.ROUTER`, `app.api.cards.ROUTER`) under `/api` via `include_routers`. Add a new resource by creating `app/api/.py` with its own `APIRouter`-based `ROUTER` and registering it there. 5. Registers `DuplicateKeyError` → 422 handler from `app/exceptions.py`. diff --git a/app/application.py b/app/application.py index ad34aeb..882bd52 100644 --- a/app/application.py +++ b/app/application.py @@ -5,7 +5,6 @@ import modern_di_fastapi from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError from lite_bootstrap import FastAPIBootstrapper -from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from app import exceptions, ioc @@ -26,10 +25,7 @@ def build_app() -> fastapi.FastAPI: di_container = modern_di.Container(groups=[ioc.Dependencies]) bootstrap_config = dataclasses.replace( settings.api_bootstrapper_config, - opentelemetry_instrumentors=[ - SQLAlchemyInstrumentor(), - AsyncPGInstrumentor(capture_parameters=True), - ], + opentelemetry_instrumentors=[SQLAlchemyInstrumentor()], ) bootstrapper = FastAPIBootstrapper(bootstrap_config=bootstrap_config) app: fastapi.FastAPI = bootstrapper.bootstrap() diff --git a/pyproject.toml b/pyproject.toml index d591356..a23c6ac 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..f48078f --- /dev/null +++ b/tests/test_tracing.py @@ -0,0 +1,41 @@ +import dataclasses +from typing import TYPE_CHECKING + +from asgi_lifespan import LifespanManager +from fastapi import status +from httpx import ASGITransport, AsyncClient +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 + + +if TYPE_CHECKING: + import pytest + + +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), AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/api/decks/") + + assert response.status_code == status.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 e74d533..d1971f0 100644 --- a/uv.lock +++ b/uv.lock @@ -255,7 +255,6 @@ dependencies = [ { name = "granian", extra = ["uvloop"] }, { name = "lite-bootstrap", extra = ["fastapi-all"] }, { name = "modern-di-fastapi" }, - { name = "opentelemetry-instrumentation-asyncpg" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "psycopg" }, { name = "pydantic-settings" }, @@ -286,7 +285,6 @@ requires-dist = [ { name = "granian", extras = ["uvloop"] }, { name = "lite-bootstrap", extras = ["fastapi-all"] }, { name = "modern-di-fastapi", specifier = ">=4,<5" }, - { name = "opentelemetry-instrumentation-asyncpg" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "psycopg" }, { name = "pydantic-settings" }, @@ -686,20 +684,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/c1/becb253043ca72ed1aa0e6c7bfda64e576e79d43019399df1e0505276e1d/opentelemetry_instrumentation_asgi-0.66b0-py3-none-any.whl", hash = "sha256:8c31fe67b2aef6f7dc3df9c0434ebd0cacd6d8545da703a55a6f160f9bc06fc5", size = 15811, upload-time = "2026-09-25T12:34:55.662Z" }, ] -[[package]] -name = "opentelemetry-instrumentation-asyncpg" -version = "0.66b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/d2/ba595debb1cba4b6239a4f19c9d67aec4ab58c9aef72fdd1ff0274a2031d/opentelemetry_instrumentation_asyncpg-0.66b0.tar.gz", hash = "sha256:69dd5e4658fb049dede8ca7eec6da2114283f5e24eef4807fdd5ef6be8a01eb8", size = 11409, upload-time = "2026-09-25T12:35:52.438Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/ec/af70a4c30ea3c2dbfa85b92fea32c82d734c9915d518e7a3648db8b6eae5/opentelemetry_instrumentation_asyncpg-0.66b0-py3-none-any.whl", hash = "sha256:1e809a0d01ae742be5137df3b53415007601b9651481d1be59b6759d3d2e2497", size = 9749, upload-time = "2026-09-25T12:34:58.705Z" }, -] - [[package]] name = "opentelemetry-instrumentation-fastapi" version = "0.66b0"