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
1 change: 1 addition & 0 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def api_bootstrapper_config(self) -> FastAPIConfig:
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,
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 5 to 8% in the same benchmark. Remove the setting if you need those spans.

## 📝 [License](https://github.com/modern-python/fastapi-sqlalchemy-template/blob/main/LICENSE)

## Part of `modern-python`
Expand Down
21 changes: 18 additions & 3 deletions tests/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from fastapi import status
from httpx import ASGITransport, AsyncClient
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace import ReadableSpan, 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
Expand All @@ -17,7 +18,7 @@
import pytest


async def test_db_queries_produce_sqlalchemy_spans(monkeypatch: pytest.MonkeyPatch) -> None:
async def _trace_list_decks(monkeypatch: pytest.MonkeyPatch) -> tuple[ReadableSpan, ...]:
traced_config = dataclasses.replace(settings.api_bootstrapper_config, opentelemetry_log_traces=True)
monkeypatch.setattr(Settings, "api_bootstrapper_config", traced_config)
exporter = InMemorySpanExporter()
Expand All @@ -30,12 +31,26 @@ async def test_db_queries_produce_sqlalchemy_spans(monkeypatch: pytest.MonkeyPat
response = await client.get("/api/decks/")

assert response.status_code == status.HTTP_200_OK
return exporter.get_finished_spans()


async def test_db_queries_produce_sqlalchemy_spans(monkeypatch: pytest.MonkeyPatch) -> None:
db_spans = [
span
for span in exporter.get_finished_spans()
for span in await _trace_list_decks(monkeypatch)
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)


async def test_request_has_no_asgi_event_spans(monkeypatch: pytest.MonkeyPatch) -> None:
request_spans = [
span
for span in await _trace_list_decks(monkeypatch)
if span.instrumentation_scope is None
or span.instrumentation_scope.name != "opentelemetry.instrumentation.sqlalchemy"
]
assert [span.kind for span in request_spans] == [SpanKind.SERVER]
Loading