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 910e480cfc
6 changed files with 60 additions and 3 deletions

View file

@ -2536,6 +2536,7 @@ That is the route's compiled regular expression, not a prettified ``/{database}/
- ``user_agent.original`` *(optional)* - The ``User-Agent`` header, verbatim. Omitted if the client sent none.
- ``http.response.status_code`` *(optional)* - The status of the response, read from the ASGI ``http.response.start`` message rather than from a :ref:`internals_response` object - several views, including static files, file downloads and streaming CSV, send that message themselves and never build one. Omitted if the connection closed before anything was sent.
- ``error.type`` *(optional)* - Set when the request failed: the exception class name if one escaped the application, otherwise the status code as a string for a 5xx response. A 4xx does **not** set this and does not set an error status - per semantic conventions a client error is not a server span's failure.
- ``datasette.internal_client`` *(optional)* - ``True`` when the request was made in-process through ``datasette.client`` rather than arriving over the network. Such a sub-request runs the full ASGI stack, so it emits its own nested ``SERVER`` span inside the outer request's - filter on this attribute to keep kind-based dashboards from double-counting requests. Omitted for real inbound requests.
``db.query``
A SQL operation issued by Datasette, covering the full round trip including any time spent queued for a thread. Callback-style calls - ``execute_fn()``, ``execute_write_fn()`` and ``execute_isolated_fn()`` - appear here too, distinguished by ``datasette.callback`` in place of ``db.query.text``.