Mark in-process datasette.client requests on their SERVER span

An internal datasette.client request runs the full ASGI stack, so it
emits a second SERVER span nested inside the outer request's - which
double-counts requests in any dashboard that counts by span kind. Rather
than downgrading the inner span to INTERNAL (which would diverge from
how httpx-ASGI instrumentation behaves and break the registry's
kind-based dynamic-name matching), the span now carries an optional
datasette.internal_client=True attribute for dashboards to filter on.

The in_datasette_client ContextVar moves to telemetry.py so the
middleware can read it without a circular import; its writers and the
in_client() accessor stay in app.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
This commit is contained in:
Alex Garcia 2026-09-02 14:20:32 -07:00
commit fb89001ab1
6 changed files with 60 additions and 3 deletions

View file

@ -856,3 +856,34 @@ def test_no_provider_takes_the_fast_path():
)
# Same fast path, other observable: nothing is stashed in the scope either.
assert report["scope_keys"] == [False, False]
@pytest.mark.asyncio
async def test_internal_client_requests_are_marked(ds, otel_spans):
"""
An in-process `datasette.client` request runs the full ASGI stack, so it
emits its own SERVER span - `datasette.internal_client` marks those so
kind-based dashboards can filter the double-count out. A request that
arrives through the raw ASGI app (the shape of a real inbound request,
without the DatasetteClient wrapper setting the ContextVar) must not
carry the attribute.
"""
otel_spans.clear()
assert (await ds.client.get("/")).status_code == 200
server = _server_spans(otel_spans)
assert server
assert all(
span.attributes.get("datasette.internal_client") is True for span in server
)
import httpx2
transport = httpx2.ASGITransport(app=ds.app())
async with httpx2.AsyncClient(
transport=transport, base_url="http://localhost"
) as client:
otel_spans.clear()
assert (await client.get("/")).status_code == 200
server = _server_spans(otel_spans)
assert server
assert all("datasette.internal_client" not in span.attributes for span in server)

View file

@ -93,6 +93,7 @@ EXPECTED_HTTP_ATTRIBUTES = {
"user_agent.original",
"http.response.status_code",
"error.type",
"datasette.internal_client",
}
# The registry's own name for the request span is that template, not anything