Remove the hand-rolled tracer: ?_trace=1, trace_debug, datasette.tracer - #2864
Draft
asg017 wants to merge 1 commit into
Draft
Remove the hand-rolled tracer: ?_trace=1, trace_debug, datasette.tracer#2864asg017 wants to merge 1 commit into
asg017 wants to merge 1 commit into
Conversation
…ground Datasette had two tracing systems since the OpenTelemetry spans landed. The hand-rolled one measures the wrong thing - issue 1730, open since 2022, is about exactly that - and it cannot be rebuilt on top of the new spans without core owning a TracerProvider, which is the one thing the OTel design refuses to do. Rather than carry duplicate instrumentation on the db.execute() hot path indefinitely, the old system goes. Deleted: datasette/tracer.py, the trace_debug setting, the AsgiTracer response-rewriting middleware and the ?_trace=1 query-string argument. - datasette/database.py: the four `with trace(...)` wrappers PR 1 deliberately nested the OTel spans inside are removed and the bodies dedented. That also retires the `# noqa: SIM117` comments those wrappers required - a leftover unnecessary noqa trips ruff's RUF100 - and `kwargs["count"] = count` in execute_write_many, which fed the old tracer only. `git diff -w` on this file shows nothing but the deleted lines. - datasette/views/base.py: stream_csv() still read ?_trace=1 to wrap CSV output in an HTML <textarea> debug page. That whole branch, including the EscapeHtmlWriter selection and the conditional content-type, is gone. The EscapeHtmlWriter class itself stays in datasette.utils - it is an importable public name and removing it would widen the API break. - .github/workflows/deploy-latest.yml no longer passes --setting trace_debug 1. Worth stating precisely, because the ticket claimed otherwise: this would not have broken the deploy. Setting.convert() in cli.py only rewrites a bare name to settings.<name> for *known* settings, so `--setting trace_debug 1` would have been silently accepted as a meaningless top-level config key. The flag is removed because it is dead, not because it errors. Tests. tests/test_tracer.py is deleted outright (6 items). Four other tests used ?_trace=1 as an assertion instrument rather than testing tracing: - test_csv_trace tested the trace mechanism itself - deleted. - test_table_csv_stream_does_not_calculate_facets, test_table_csv_stream_does_not_calculate_counts and test_nocount_nofacet_if_shape_is_object test real behaviour, and are rebuilt against captured spans. All three had silently stopped being able to fail: the facets test looked for "select content, count(*) as n", which facet suggestion has not emitted since it moved to a `with limited as (...)` CTE, and none of the three requested the count or facet work whose suppression they claim to check. The rebuilt versions ask for it explicitly, match strings the current SQL contains, and carry a guard assertion so an empty span list cannot masquerade as a pass. Each was confirmed to fail with the covered code broken. - test_trace_correctly_escaped is kept, renamed test_query_page_escapes_sql, with ?_trace=1 dropped. It ran against ds_client, which has no trace_debug, so it never exercised the tracer - what it actually covered is the query page echoing user SQL into HTML, the surface of the two reflected-XSS advisories in issue 1360, and nothing else in the suite covers it. Deleting it would have quietly dropped that. tests/test_utils.py's pairs_to_nested_config case used settings.trace_debug to check that a later key overrides an earlier one; it now uses template_debug rather than losing the case. Docs: the datasette.tracer section of internals.rst, the trace_debug section of settings.rst, the ?_trace=1 entries in json_api.rst and introspection.rst, and the regenerated cli-reference.rst. changelog.rst gets a breaking-change entry and keeps all its historical ?_trace=1 entries - two of them had to lose a :ref: role pointing at a label this commit deletes, or Sphinx warns on every build. 2368 passed, 39 skipped, 6 xfailed, 15 xpassed, 140 subtests, against 2375 / 141 before. Net -7 tests, fully accounted for: -6 test_tracer.py, -1 test_csv_trace, -1 test_trace_correctly_escaped, +1 test_query_page_escapes_sql. The lost subtest is the per-setting case trace_debug generated in test_settings_are_documented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## asg017/otel-phase1-2-request-span #2864 +/- ##
===================================================================
Coverage ? 0.00%
===================================================================
Files ? 74
Lines ? 12430
Branches ? 0
===================================================================
Hits ? 0
Misses ? 12430
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This removes Datasette's hand-rolled tracer now that OpenTelemetry covers the same ground:
datasette/tracer.py(156 lines) —trace(),trace_child_tasks(),capture_traces(),AsgiTracertrace_debugsetting?_trace=1query-string argument, in both HTML and JSON responsesdatasette.tracersection of the internals documentationMost of the
datasette/database.pychurn is a one-level dedent:git diff -w datasette/database.pyshows only the deleted
with trace(...)lines, the deleted import, and one stale comment.This is a user-facing removal and a plugin-API removal. I have tried to make the case below
honestly, including the parts that argue against it. If the trade isn't worth it, the right outcome
is to close this and keep both — PRs 1 and 2 stand on their own and don't depend on this landing.
What is actually lost
Not "nothing, use the console exporter". Specifically:
?_trace=1todayopentelemetry-instrument, env vars, and somewhere to send datatraceback.extract_stack(limit=6)) — "which line issued this?"The traceback is the one I'd most want back, and nothing in the OpenTelemetry work replaces it.
from datasette.tracer import traceandtrace_child_tasks()are documented plugin APIs(
docs/internals.rst), so this is a breaking change for plugins, not only for users. Any pluginimporting them raises
ModuleNotFoundErrorafter this.datasette-pretty-tracesis linked from the internals docs as the recommended way to read thisoutput, and it does break — but not by import. Its source doesn't touch
datasette.tracerat all;it gates on
request.args.get("_trace") and datasette.setting("trace_debug").Datasette.setting()returns
Nonefor an unknown key rather than raising, so after this PR the plugin installs andimports fine and simply never injects its JavaScript again. That is a quieter failure than an
ImportError, which arguably makes a coordinated release more important, not less. I'm happy tosend that PR.
GitHub code search, run at time of writing (
gh api search/code):"from datasette.tracer import"simonw/datasette,simonw/datasette-private,simonw/docs-for-llms,fulcrumresearch/datasette(a fork),PyTables/datasette-connectors"trace_child_tasks"AnantAgarwal07/civic-ai,SBC1357/xiake-island,itcrowdsource/vcftocsvcontactexporter,tushkum34-cloud/pylibsmeta)The high hit counts are almost entirely Datasette's own repositories, forks of them, and copies of
Datasette's documentation — GitHub counts files, not projects. The only third-party package in
either list is
PyTables/datasette-connectors. I have not audited it; someone should before thismerges.
"_trace=1"returns 8,424 hits, but that string is far too generic to draw any conclusionfrom and I'm not going to pretend otherwise.
latest.datasette.iois deployed with--setting trace_debug 1(
.github/workflows/deploy-latest.yml), so that workflow changes in this PR. To be precise aboutwhat would happen if it didn't:
--setting <bare-name> <value>only rewrites tosettings.<name>when
<name>is a known setting (datasette/cli.py,Setting.convert). Oncetrace_debugisgone,
--setting trace_debug 1is silently accepted as a meaningless top-level config key — noerror, no warning, deploy stays green with a dead flag. (
--setting settings.trace_debug 1doesfail loudly:
Error: Invalid setting 'trace_debug' in config file.) The flag is removed here becauseit is dead, not because leaving it would break the deploy.
The docs that link to
https://latest.datasette.io/fixtures/roadside_attractions?_trace=1changetoo.
The case for removing it anyway
?_trace=1cannot be rebuilt on OpenTelemetry without core owning a provider. To be precise,since it matters: the existing hand-rolled tracer coexists with the OpenTelemetry work perfectly
well — they are independent code paths, which is exactly why the previous two PRs left it running.
What does not work is the tempting version of this, where
?_trace=1is reimplemented on top of thenew spans. That needs Datasette to collect spans in-process and render them into a response, which
means core owns a
TracerProvider. An earlier draft of this work did exactly that, and that singlerequirement produced three problems in self-review:
the SDK has no
remove_span_processor, andProxyTracercaches its concrete tracer permanently,so resets don't work
Deleting
?_trace=1dissolved all three. The invariant that replaced it — core depends onopentelemetry-apionly and never creates a provider, exporter or sampler — is what makes therest of this work cheap and safe, and it's enforced by a test.
So the honest form of the argument is narrow: core cannot own this feature any more. It says
nothing against the browser experience existing as a plugin — one owning its own provider and an
in-memory span collector could serve much the same page, out of core, where owning a provider is
fine. I haven't built that, and I'd rather say so than imply the option doesn't exist. See "If you'd
rather not" below.
The timing argument. Datasette is in the 1.0 alpha series. Removing a documented plugin API is
dramatically cheaper before 1.0 final than after it. If this is going to happen at all, now is when
it costs least.
It's carrying real baggage.
AsgiTracerbuffers up to 256KB of every response body to splicetrace output into it, and still carries a
# TODO: What to do about Content-Type or other cases?.?_trace=1was the subject of a reflected-XSS security release (1360). It has been default-offsince 0.57.
It measures the wrong thing, which is a known open issue.
#1730 — "SQL tracing should much more closely
track the SQL query execution" — has been open since 2022, and the hand-rolled tracer is what it is
about. PRs 1 and 2 built the correct measurement alongside it:
db.querycovers the full round tripand
db.query.executecovers only the work inside the worker thread, so the gap between them isexactly the thread-pool wait the current tracer folds invisibly into one number. This PR is what
actually closes #1730, by retiring the measurement that was wrong rather than leaving two in
place.
What's left behind
datasette/views/base.pystill readsrequest.args.get("_trace")to wrap CSV output in an HTML<textarea>debug page. Removed here.tests/plugins/my_plugin.pykeeps its/parallel-queriesroute, now with thetrace_child_tasks()wrapper gone. Nothing exercises it any more — its only test lived intest_tracer.pyand the docs section that referenced it is deleted. Left in place as a fixture.?_trace=1entries, obviously — only forward-looking docschange.
DATASETTE_TRACE_PLUGINSis not touched by this PR. It's a separate mechanism with a separateissue history.
datasette.utils.EscapeHtmlWriternow has no caller anywhere in core — the CSV<textarea>debugpage was its only use. It is left in place because it is an importable name in
datasette.utilsand removing it would widen the API break beyond what this PR advertises. Say the word and it goes.
A note on the three rebuilt tests
Three tests used
?_trace=1as an assertion instrument rather than testing tracing, so they arerebuilt against spans instead of deleted:
test_table_csv_stream_does_not_calculate_facets,test_table_csv_stream_does_not_calculate_countsandtest_nocount_nofacet_if_shape_is_object.While rebuilding them I checked each could still fail, and found that all three had stopped being
able to fail some time ago — they were passing for reasons unrelated to what they claim to test:
select content, count(*) as n. Facet suggestionhasn't emitted that shape since it moved to a
with limited as (...)CTE; the modern query isselect content as value, count(*) as n from limited. The needle matched nothing, so theassertion was unconditionally true.
.csvURL. On currentmaina CSV request resolves nocountor
suggested_facetsextra at all, so neither query runs regardless of the_nofacet=1/_nocount=1injection instream_csv()that the tests were nominally guarding.test_nocount_nofacet_if_shape_is_objectasserted"count(*)" not in response.texton?_shape=objectwithout asking for a count in the first place — so the_shape in ("array", "object") → nocount = Truebranch it is named after was never exercised.The rebuilt versions ask for the work explicitly (
?_shape=object&_extra=count&_facet=state), matchon strings the current SQL actually contains, and each carries a guard assertion so an empty span
list can't masquerade as a pass. I verified each one fails when the code it covers is broken, and
passes again when it is restored.
One more test in the same category:
test_trace_correctly_escapedrequested/fixtures/-/query?sql=select+'<h1>Hello'&_trace=1usingds_client, which has notrace_debugset — so
?_trace=1did nothing and the test never touched the tracer. What it did cover is thequery page echoing user-supplied SQL back into HTML, which is the exact surface of the two
reflected-XSS advisories in issue 1360, and nothing else in the suite covers it. Rather than delete
it with the rest, it is kept as
test_query_page_escapes_sqlwith the?_trace=1dropped. It failsif
autoescapeis turned off.Separately, and not fixed here: for a CSV request
table_view_traced()callstable_view_data()once with the unmodified request before
stream_csv()builds the_nofacet=1&_nocount=1request,so on
/t.csv?_extra=countthe count query runs once anyway. The injection halves the work ratherthan preventing it. That predates this PR and is a separate issue.
If you'd rather not
Reasonable outcomes other than merging, ranked by what I'd pick:
datasette-otel-inspectplugin owning itsown provider and an in-memory collector, giving back the in-browser per-request view without core
owning a provider. Say the word and I'll build it before this merges rather than promising it
afterwards.
?_trace=1for a release with a deprecation warning, remove after1.0. The real cost is carrying two tracing systems through the window, not any provider risk —
the old tracer owns no provider.
1 and 2 removed nothing. The cost is permanent duplicate instrumentation on the
db.execute()hot path and two things to keep working.
I'd rather any of these than have the PR sit open.
Migration note for the changelog
For anyone using
?_trace=1today, the nearest equivalent with no collector:Terminal rather than browser, whole process rather than one request. That gap is the regression
above, stated plainly rather than dressed up.
How to test this
Stack created with GitHub Stacks CLI • Give Feedback 💬