datasette/docs
Alex Garcia 8b35d4a80d Add OpenTelemetry metrics for SQL thread pool saturation and query latency
Spans describe requests that have finished. They structurally cannot answer
"am I saturating my 3 SQL threads right now", because that is a level rather
than an event - and with num_sql_threads defaulting to 3, it is usually the
first thing worth knowing about a busy Datasette. This adds the metrics that
answer it.

Five observable gauges, computed only when something is collecting, so an
instance with no MeterProvider installed does no work for them at all:

  datasette.sql.threads.limit         num_sql_threads
  datasette.sql.threads.queue_depth   queries waiting for a free thread
  datasette.sql.queries.pending       in-flight reads, by db.namespace
  datasette.write.queue_depth         writes behind the single write thread
  datasette.connections.open          tracked file connections

Three instruments recorded inline, which matters because metrics survive
trace sampling and spans do not - an operator sampling 1% of traces still
gets 100% of the latency distribution:

  db.client.operation.duration        semconv histogram, with error.type
  datasette.write.queue_wait          the metric twin of the existing span
  datasette.sql.queries.interrupted   sql_time_limit_ms kills

The interrupted counter closes a gap the plan called out as unanswerable:
"how often are we killing queries at the limit" is a rate, and a rate cannot
be recovered from sampled spans.

Core still creates no provider of any kind, so the architecture is unchanged;
`grep -rn 'opentelemetry.sdk' datasette/` stays empty. One real difference
from tracing is worth recording: _ProxyMeter and its instruments forward to a
provider installed after they were created, whereas ProxyTracer permanently
caches the first concrete tracer it resolves. Module-level instruments are
therefore safe and the test fixture has no ordering constraint.

Live instances are tracked in a lock-guarded WeakSet so instrumenting an
instance never keeps it alive. The pool gauges carry no attribute saying
which Datasette produced them: production runs one instance per process, and
adding an id to disambiguate the test suite's hundreds of instances would buy
unbounded attribute cardinality to fix a case that does not occur. The
collision is documented instead, and the gauge callbacks are plain generator
functions so tests can assert exact values by calling them directly rather
than through the SDK's last-value aggregation.

demos/otel/metrics_demo.py fires 12 concurrent 40ms queries at a 3-thread
pool and samples the gauges mid-flight: queue_depth peaks at exactly 9, and
the duration histogram reads max=0.1695s for a query whose work is 40ms. That
gap is the queue, and it is the thing traces alone will not show you.

Also corrects the demo README's privacy section, which still claimed
parameter values are never recorded - that stopped being unconditionally true
when trace_sql_parameters landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

(cherry picked from 6ef0dd8c and adapted to the rebuilt phase-1 stack:
attribute names now come from telemetry_registry where entries exist, the
meter carries the instrumentation-scope version and schema URL, and the
interrupted-queries counter skips expected timeouts - callers that opted
into a deliberately short budget, like facet suggestion - matching how
those are excluded from span error status. The internals.rst reference
lands with the registry commit that follows.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2h9ANGZ7paWSpqs5DUAcG
2026-09-01 17:09:35 -07:00
..
_static Add favicon to documentation (#1967) 2022-12-31 11:00:31 -08:00
_templates Drop jQuery dependency 2023-03-26 16:38:58 -07:00
.gitignore Added initial docs, including a changelog 2017-11-16 07:11:00 -08:00
authentication.rst Better permission debug tools and documentation 2026-07-14 08:40:07 -07:00
auto-build.sh Added --load-extension argument to datasette serve 2017-11-16 08:48:49 -08:00
binary_data.rst Support BLOB values in row edit UI 2026-07-03 16:09:27 -07:00
changelog.rst Add OpenTelemetry metrics for SQL thread pool saturation and query latency 2026-09-01 17:09:35 -07:00
cli-reference.rst Remove the hand-rolled tracer now that OpenTelemetry covers the same ground 2026-09-01 16:24:35 -07:00
codespell-ignore-words.txt Move Metadata to --internal database 2024-06-11 09:33:23 -07:00
conf.py Upgrade to ruff>=0.16.0 (#2857) 2026-07-25 15:47:08 -07:00
configuration.rst textarea column type 2026-06-13 22:18:45 -07:00
contributing.rst Add cache-busted static asset helper (#2804) 2026-06-23 13:44:58 -07:00
csv_export.rst Use shot-scraper images from datasette-screenshots repo, closes #1844 2022-10-14 12:57:00 -07:00
custom_templates.rst Improve docs structure for static(), refs #2804, #2800 2026-06-23 13:59:01 -07:00
datasette-0.51.png Release 0.51 2020-10-31 15:21:49 -07:00
datasette-logo.svg Added new logo to the documentation 2020-07-12 12:53:29 -07:00
deploying.rst Add test for RST heading underline lengths, closes #2544 2025-10-26 09:49:49 -07:00
ecosystem.rst Shrunk ecosystem docs in favour of datasette.io, closes #1182 2021-01-09 14:17:18 -08:00
events.md RenameTableEvent, plus write connection track_event() mechanism (#2682) 2026-03-30 11:20:46 -07:00
facets.rst Move table configuration docs from metadata.rst to configuration.rst (#2668) 2026-03-17 08:47:04 -07:00
full_text_search.rst Move table configuration docs from metadata.rst to configuration.rst (#2668) 2026-03-17 08:47:04 -07:00
getting_started.rst Replace Glitch with Codespaces, closes #2488 2025-05-28 19:17:22 -07:00
index.rst Generated template context documentation, closes #1510 2026-06-11 07:47:15 -07:00
installation.rst Remove Datasette Desktop from installation guide 2026-07-07 14:04:46 -07:00
internals.rst Remove the hand-rolled tracer now that OpenTelemetry covers the same ground 2026-09-01 16:24:35 -07:00
introspection.rst Remove the hand-rolled tracer now that OpenTelemetry covers the same ground 2026-09-01 16:24:35 -07:00
javascript_plugins.rst Better docs for makeJumpSections() 2026-06-14 16:28:09 -07:00
json_api.rst Remove the hand-rolled tracer now that OpenTelemetry covers the same ground 2026-09-01 16:24:35 -07:00
json_api_doc.py Upgrade to ruff>=0.16.0 (#2857) 2026-07-25 15:47:08 -07:00
Makefile Document custom json encoder (#1996) 2026-06-23 16:24:03 -07:00
metadata.rst Move table configuration docs from metadata.rst to configuration.rst (#2668) 2026-03-17 08:47:04 -07:00
metadata_doc.py Upgrade to ruff>=0.16.0 (#2857) 2026-07-25 15:47:08 -07:00
pages.rst Unify page-size parameters on _size with table semantics 2026-07-04 17:54:03 +00:00
performance.rst Release 0.63a1 2022-10-23 20:07:09 -07:00
plugin_hooks.rst Return 401 for invalid or expired bearer tokens 2026-07-04 15:18:12 +00:00
plugins.rst Advise plugin authors on naming secret configuration keys 2026-07-06 23:09:10 +00:00
publish.rst Remove all remaining "$ " prefixes from docs, closes #2140 2023-08-11 10:44:34 -07:00
settings.rst Remove the hand-rolled tracer now that OpenTelemetry covers the same ground 2026-09-01 16:24:35 -07:00
spatialite.rst Renamed canned queries to queries / stored queries in docs 2026-05-26 15:17:51 -07:00
sql_queries.rst Return 400 for write canned-query SQL failures 2026-07-04 14:35:54 +00:00
telemetry_doc.py Name every span and attribute once, in a registry the docs are built from 2026-09-01 16:24:35 -07:00
template_context.rst Remove the next_url extra - the key is always present 2026-07-06 23:59:53 +00:00
template_context_doc.py Upgrade to ruff>=0.16.0 (#2857) 2026-07-25 15:47:08 -07:00
testing_plugins.rst datasette.fixtures module, closes #2733 2026-05-21 23:05:37 -07:00
upgrade-1.0a20.md Renamed canned queries to queries / stored queries in docs 2026-05-26 15:17:51 -07:00
upgrade_guide.md Replace token-based CSRF with Sec-Fetch-Site header protection (#2689) 2026-04-14 17:11:36 -07:00
writing_plugins.rst Add cache-busted static asset helper (#2804) 2026-06-23 13:44:58 -07:00