datasette/docs/plugin_telemetry.rst

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

266 lines
14 KiB
ReStructuredText
Raw Normal View History

Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry:
Telemetry for plugin authors
============================
Datasette core emits OpenTelemetry spans and metrics for the work it does itself - see :ref:`internals_telemetry` for what those are and how an operator turns them on. This page is about the other half: instrumenting the work **your plugin** does, so that a plugin's queries, background jobs and custom operations show up in the same traces and the same metrics pipeline, using the same conventions.
Depend on ``opentelemetry-api`` only. Providers and exporters are configured by whoever runs Datasette. Without a provider, no telemetry is recorded.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry_scope:
Use your own instrumentation scope
----------------------------------
Never emit through core's tracer or meter. Your plugin's scope name is the machine-readable claim about *who emitted a signal*, and consumers filter on it:
.. code-block:: python
from opentelemetry import metrics, trace
from my_plugin import __version__
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
tracer = trace.get_tracer("my_plugin", __version__)
meter = metrics.get_meter("my_plugin", __version__)
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
If every attribute you emit follows current semantic conventions you can also pass ``schema_url=``; ``datasette.telemetry.SCHEMA_URL`` is the version core's own spellings track, with a comment explaining how to choose one. When in doubt, omit it - a wrong schema URL is worse than none.
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
Two naming rules keep the ecosystem's signals tellable-apart:
- **Scope**: use your plugin's *import package* name - ``my_plugin``, underscores and all. Consumers filter on the scope, and one spelling convention means they can guess it.
- **Signal prefix**: name spans, metrics and custom attributes under a prefix you own - your package name (``my_plugin.*``) or a short product name (``paper.*``). **Never a bare** ``datasette.*`` **prefix**: that namespace belongs to core, an operator could no longer tell core signals from plugin signals, and a future core signal could collide with yours.
Reuse core's shared attribute spellings where they mean the same thing - ``db.namespace`` for a database name, ``error.type`` for a failure class - rather than minting parallel ones. If a span family in your registry shares a prefix with another entry, exact names always win over prefix matches, but two overlapping ``prefix=True`` entries resolve to whichever is listed first - avoid overlapping families rather than relying on order.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry_registry:
Declare a registry
------------------
Core keeps a single source of truth for every signal it emits in ``datasette/telemetry_registry.py``, and the classes it uses are public API. They subclass ``str``, so a registry entry *is* the name you pass to OpenTelemetry - no parallel constants to keep in step:
.. code-block:: python
from opentelemetry.trace import SpanKind
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
from datasette.telemetry_registry import (
Attribute,
MetricName,
SpanName,
)
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
OUTCOME = Attribute(
"my_plugin.outcome",
"How the job ended.",
values={"ok", "error", "skipped"},
)
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
JOB_NAME = Attribute(
"my_plugin.job", "The registered job name."
)
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
JOB_RUN = SpanName(
"my_plugin.job.run",
"One execution of a scheduled job.",
(OUTCOME, JOB_NAME),
)
# A span family with a variable suffix - emitted as "my_plugin.chat gpt-5"
CHAT = SpanName(
"my_plugin.chat ",
"One model call, named ``my_plugin.chat {model}``.",
prefix=True,
)
SPANS = (JOB_RUN, CHAT)
JOB_DURATION = MetricName(
"my_plugin.job.duration",
"Histogram",
"s",
"How long each job took.",
(JOB_NAME, OUTCOME),
buckets=(0.01, 0.1, 1, 10, 60, 600, 3600),
)
Three details that matter:
- ``values=`` declares a **closed enum**. The conformance helpers (below) assert every emitted value is a member, which is what makes an attribute safe to use as a metric dimension - a metric series is keyed by its attribute values, so an open value set on a metric is an unbounded-cardinality hazard.
- ``prefix=True`` registers a span *family* whose emitted names share a fixed prefix; ``datasette.telemetry_registry.span_for()`` matches them by prefix, exact names first.
- Declare explicit histogram ``buckets=`` scaled to *your* domain. Core's SQLite-scale boundaries are importable as ``datasette.telemetry_registry.DURATION_BUCKETS`` (0.0001s to 10s) - use them if you are timing SQLite work so dashboards align, and define your own otherwise (a job scheduler wants buckets out to an hour; the SDK's defaults will put all your measurements in one bucket either way).
.. _plugin_telemetry_privacy:
Privacy and cardinality rules
-----------------------------
Core's instrumentation records **no data users put into Datasette and no identifier that ties a signal to a person** - no parameter values, no query strings, no actor identifiers, no IP addresses. Hold your plugin to the same bar:
- Attribute values should be closed enums, booleans, counts and durations. Anything echoed from user input - a name, a URL, a token, free text - does not belong on a span, and *especially* not on a metric.
- If you time user-influenced SQL, follow core: record the SQL via ``datasette.telemetry.sql_attribute()`` (truncated, never parameters) on spans only.
- When a value is interesting but unbounded, record a bounded proxy instead: a count, a byte size, a truncation flag, or the enum outcome.
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
These rules are enforceable: see ``assert_no_forbidden_values()`` in :ref:`plugin_telemetry_testing`.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry_callbacks:
Your database work is already traced
------------------------------------
Every call your plugin makes through :ref:`db.execute() <database_execute>`, :ref:`db.execute_fn() <database_execute_fn>`, :ref:`db.execute_write() <database_execute_write>` and :ref:`db.execute_write_fn() <database_execute_write_fn>` already emits core's ``db.query`` spans and is counted in the ``db.client.operation.duration`` histogram. Two consequences:
- **Pass named callables**, not lambdas: the span for a callback-style call is identified by ``datasette.callback``, the callable's qualified name, and a lambda reports ``<lambda>``.
- If you also wrap those calls in your own span or histogram, you are creating a *second* series in *your* scope - that is fine and sometimes right (yours can carry plugin-level attributes core cannot know), but it is a deliberate two-series design, not a substitute for core's.
.. _plugin_telemetry_request_span:
Enriching the request span
--------------------------
Inside a view or ASGI middleware, ``datasette.telemetry.request_span(scope)`` returns the recording ``SERVER`` span for the current request, or ``None`` when nothing is recording - which is also your signal to skip any work done only to compute attributes:
.. code-block:: python
from datasette.telemetry import request_span
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
async def my_view(request):
span = request_span(request.scope)
if span is not None:
span.set_attribute("my_plugin.cache", "hit")
...
.. _plugin_telemetry_background:
Background work: roots with links
---------------------------------
A background job, a scheduled task or a queue consumer must **not** parent its spans to the request that caused it - by the time the work runs, that request span has usually ended, and a child outliving its closed parent renders badly in every major trace UI. The correct shape, the one core itself uses for ``execute_write(block=False)``, is a **root span carrying a link** to the causing span:
.. code-block:: python
from datasette.telemetry import linked_root_span_kwargs
# Capture at scheduling time, while the causing span is current:
kwargs = linked_root_span_kwargs()
# Later, wherever the work actually runs:
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
with tracer.start_as_current_span(
"my_plugin.job.run", **kwargs
) as span:
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
span.set_attribute(OUTCOME, "ok")
For a periodic loop (a health check, a scheduler tick), the convention is one root span **per tick**, always emitted - including no-op ticks, with an outcome attribute saying so - plus a tick counter metric. Suppressing quiet ticks seems tidy but destroys the signal operators actually want: "is the loop still running?". Pair the spans with a gauge for the loop's staleness if the interval is long.
Two propagation facts worth knowing (details in ``datasette/telemetry.py``):
- Core's ``tracer`` and yours are proxies. A ``ProxyTracer`` permanently caches the first concrete tracer it resolves *after* a provider exists, so in embedded deployments the provider must be installed before the first span - importing the module is fine, starting spans is not. Meters forward retroactively; tracers do not.
- ``asyncio.create_task`` copies the ambient context, so a long-running task created during a request will silently parent to that request's span - exactly the bug ``linked_root_span_kwargs()`` exists to avoid.
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
.. _plugin_telemetry_gauges:
Observable gauges
-----------------
For a *level* - how many streams are open, how deep is a queue - register an observable gauge whose callback the SDK invokes on its own collection cycle. Three disciplines, all inherited from how core implements its pool gauges in ``datasette/telemetry.py``:
- Hold live objects **weakly** (a ``weakref.WeakSet`` guarded by a lock), so instrumenting an object never keeps it alive, and unregister on close.
- The callback runs on the SDK's **collection thread**: never take a lock the request path holds, never await, never do I/O. Read cached state and yield ``Observation`` values; if freshness matters, refresh the cache from your own code and expose its staleness as another gauge.
- With no provider installed the callback is **never invoked at all**, so gauges are free by default.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry_testing:
Testing your instrumentation
----------------------------
2026-09-24 11:16:17 -07:00
Use ``datasette.telemetry_testing`` to capture telemetry in your tests and check it against your registry. Add `opentelemetry-sdk <https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk>`__ to your test dependencies, then import these fixtures in ``conftest.py``:
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. code-block:: python
from datasette.telemetry_testing import ( # noqa: F401
otel_metrics,
otel_meter_provider,
otel_provider,
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
otel_reset,
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
otel_spans,
)
2026-09-24 11:16:17 -07:00
``otel_provider`` and ``otel_meter_provider``
Automatically configure in-memory recording for spans and metrics once per test session.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
2026-09-24 11:16:17 -07:00
``otel_reset``
Automatically clears recorded spans and drains collected metrics after every test.
``otel_spans``
Provides an ``InMemorySpanExporter``. Call ``get_finished_spans()`` to retrieve spans recorded during the test.
``otel_metrics``
Provides a metrics collector. Call ``collect()`` to capture a snapshot, then use ``point()`` or ``points()`` to inspect it.
Tests requesting ``otel_spans`` or ``otel_metrics`` skip if the SDK is unavailable or another provider has already been installed.
The assertion helpers check the recorded telemetry against your registry:
``assert_spans_conform()``
Checks that emitted spans and attributes are registered, and attribute values match any declared ``values=`` enums.
``assert_metrics_conform()``
Checks that emitted metrics and attributes are registered, attribute values match any declared enums, and instrument kinds and units match the registry.
``assert_spans_covered()`` and ``assert_metrics_covered()``
Check that every registered span or metric and its required attributes appeared during the test. Attributes marked ``optional=True`` are excluded from this check; test those separately.
Pass your plugin's instrumentation scope as ``scope_name`` to these helpers, since the fixtures also record Datasette's own telemetry.
Run a workload that exercises your instrumentation, then call ``otel_metrics.collect()`` once before checking the metrics. Counters and histograms report measurements since the previous collection. Keep the Datasette instance open until collection so observable gauges can report its state:
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. code-block:: python
from datasette.telemetry_testing import (
Extend the kit with metric-side conformance: kind, unit and enum checks The five surveyed plugin plans all kept a hand-rolled metrics-vs-registry diff because the kit's conformance helpers covered spans only. This adds the metric side: - metric_for() in the registry (the span_for analogue - no prefix/dynamic machinery, metric names are static), and the attribute helpers are documented as accepting MetricName entries. - MetricsCollector.collect() now retains the instrumentation scope per collected metric, so a plugin is judged against its own meter only. - assert_metrics_conform(): every collected metric in scope is registered, was created as the instrument kind and unit its registry entry declares (drift between the registry entry and the meter.create_*() call was previously caught by nothing, in core or any plugin), sets only registered attributes, and respects values= enums - the check that makes a metric dimension provably bounded. - assert_metrics_covered(): every registered metric collected at least once with every non-optional attribute seen. Both *_covered helpers now exempt optional=True attributes, so a workload is not forced to manufacture every error path; pin those with targeted tests instead. - datasette.operation declares values={"read", "write"} - core dogfoods the enum enforcement on the dimension where it matters most. - Core's generic metric conformance tests are now calls to the kit helpers with scope_name="datasette"; the stricter literal-pinning and optional-attribute-coverage tests stay hand-written on purpose. - The metric reference docs render attributes through the same helper as spans, so *(optional)* markers and enum values now appear there too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:55:47 -07:00
assert_metrics_conform,
assert_metrics_covered,
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
assert_package_never_imports_sdk,
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
assert_spans_covered,
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
assert_spans_conform,
)
Extend the kit with metric-side conformance: kind, unit and enum checks The five surveyed plugin plans all kept a hand-rolled metrics-vs-registry diff because the kit's conformance helpers covered spans only. This adds the metric side: - metric_for() in the registry (the span_for analogue - no prefix/dynamic machinery, metric names are static), and the attribute helpers are documented as accepting MetricName entries. - MetricsCollector.collect() now retains the instrumentation scope per collected metric, so a plugin is judged against its own meter only. - assert_metrics_conform(): every collected metric in scope is registered, was created as the instrument kind and unit its registry entry declares (drift between the registry entry and the meter.create_*() call was previously caught by nothing, in core or any plugin), sets only registered attributes, and respects values= enums - the check that makes a metric dimension provably bounded. - assert_metrics_covered(): every registered metric collected at least once with every non-optional attribute seen. Both *_covered helpers now exempt optional=True attributes, so a workload is not forced to manufacture every error path; pin those with targeted tests instead. - datasette.operation declares values={"read", "write"} - core dogfoods the enum enforcement on the dimension where it matters most. - Core's generic metric conformance tests are now calls to the kit helpers with scope_name="datasette"; the stricter literal-pinning and optional-attribute-coverage tests stay hand-written on purpose. - The metric reference docs render attributes through the same helper as spans, so *(optional)* markers and enum values now appear there too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:55:47 -07:00
from my_plugin.telemetry import METRICS, SPANS
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
2026-09-24 11:16:17 -07:00
def test_api_only_dependency():
assert_package_never_imports_sdk("my_plugin")
Extend the kit with metric-side conformance: kind, unit and enum checks The five surveyed plugin plans all kept a hand-rolled metrics-vs-registry diff because the kit's conformance helpers covered spans only. This adds the metric side: - metric_for() in the registry (the span_for analogue - no prefix/dynamic machinery, metric names are static), and the attribute helpers are documented as accepting MetricName entries. - MetricsCollector.collect() now retains the instrumentation scope per collected metric, so a plugin is judged against its own meter only. - assert_metrics_conform(): every collected metric in scope is registered, was created as the instrument kind and unit its registry entry declares (drift between the registry entry and the meter.create_*() call was previously caught by nothing, in core or any plugin), sets only registered attributes, and respects values= enums - the check that makes a metric dimension provably bounded. - assert_metrics_covered(): every registered metric collected at least once with every non-optional attribute seen. Both *_covered helpers now exempt optional=True attributes, so a workload is not forced to manufacture every error path; pin those with targeted tests instead. - datasette.operation declares values={"read", "write"} - core dogfoods the enum enforcement on the dimension where it matters most. - Core's generic metric conformance tests are now calls to the kit helpers with scope_name="datasette"; the stricter literal-pinning and optional-attribute-coverage tests stay hand-written on purpose. - The metric reference docs render attributes through the same helper as spans, so *(optional)* markers and enum values now appear there too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:55:47 -07:00
def test_conformance(otel_spans, otel_metrics):
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
run_a_workload_that_exercises_everything()
finished = otel_spans.get_finished_spans()
# Everything emitted is registered (and enum values are legal):
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
assert_spans_conform(
SPANS, finished, scope_name="my_plugin"
)
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
# Everything registered was emitted:
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
assert_spans_covered(
SPANS, finished, scope_name="my_plugin"
)
2026-09-24 11:16:17 -07:00
# Collect once, then check the metrics:
Extend the kit with metric-side conformance: kind, unit and enum checks The five surveyed plugin plans all kept a hand-rolled metrics-vs-registry diff because the kit's conformance helpers covered spans only. This adds the metric side: - metric_for() in the registry (the span_for analogue - no prefix/dynamic machinery, metric names are static), and the attribute helpers are documented as accepting MetricName entries. - MetricsCollector.collect() now retains the instrumentation scope per collected metric, so a plugin is judged against its own meter only. - assert_metrics_conform(): every collected metric in scope is registered, was created as the instrument kind and unit its registry entry declares (drift between the registry entry and the meter.create_*() call was previously caught by nothing, in core or any plugin), sets only registered attributes, and respects values= enums - the check that makes a metric dimension provably bounded. - assert_metrics_covered(): every registered metric collected at least once with every non-optional attribute seen. Both *_covered helpers now exempt optional=True attributes, so a workload is not forced to manufacture every error path; pin those with targeted tests instead. - datasette.operation declares values={"read", "write"} - core dogfoods the enum enforcement on the dimension where it matters most. - Core's generic metric conformance tests are now calls to the kit helpers with scope_name="datasette"; the stricter literal-pinning and optional-attribute-coverage tests stay hand-written on purpose. - The metric reference docs render attributes through the same helper as spans, so *(optional)* markers and enum values now appear there too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:55:47 -07:00
otel_metrics.collect()
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
assert_metrics_conform(
METRICS, otel_metrics, scope_name="my_plugin"
)
assert_metrics_covered(
METRICS, otel_metrics, scope_name="my_plugin"
)
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
2026-09-24 11:16:17 -07:00
``assert_package_never_imports_sdk()`` checks that importing your plugin does not import the OpenTelemetry SDK. Run this test early in your suite; see the helper's docstring for a macOS threading limitation.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
2026-09-24 11:16:17 -07:00
Use ``assert_no_forbidden_values()`` to check for private data in telemetry. Include fake email addresses, tokens or usernames in your test workload, then pass those values, the finished spans and the collected metrics to the helper. It checks span names, attributes, events, status descriptions and metric attributes.
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
2026-09-24 11:16:17 -07:00
Leave ``scope_name`` unset for privacy checks so they include both your plugin's telemetry and Datasette's own.
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
Add a plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs A survey of five plugin OTel plans (datasette-paper, -agent, -litestream, -accounts, -cron) found every one hand-copying the same core machinery: the registry classes, the conformance-test harness, the pytest fixtures, the bucket boundaries and the detached-root-with-Link recipe. This makes that machinery importable instead: - The registry classes are documented public API. Attribute gains values= (a closed enum the conformance helpers enforce - what makes an attribute safe as a metric dimension); SpanName gains prefix=True for span families like "chat {model}" whose names share a fixed prefix, matched by span_for() after exact names. span_for()/attribute helpers accept a spans= tuple so plugin registries can use them. - datasette.telemetry.linked_root_span_kwargs(): the root-span-with-Link shape for work a request caused without containing - background jobs, scheduled ticks, block=False writes. Core's own write thread now uses it instead of building the kwargs inline. - datasette.telemetry_testing: the session provider fixtures, otel_spans / otel_metrics, a two-way registry conformance checker (including enum and prefix handling, filtered by instrumentation scope) and an assert_package_never_imports_sdk() guard. Core's conftest now imports these instead of defining them, so the suite consumes the kit exactly as a plugin's would. - New "Telemetry for plugin authors" docs page: scope discipline, registry usage, privacy/cardinality rules, named-callable guidance, request_span(), the background root-with-link convention (one root per tick, always emitted), provider-ordering facts and known caveats. request_span() is now documented public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 12:24:42 -07:00
.. _plugin_telemetry_caveats:
Known caveats
-------------
- **Streaming responses hold the request span open.** Core's request span ends when the response body finishes, so for an SSE or long-streaming route its duration is the connection lifetime. If you need per-message timing on a stream, emit your own child spans or span events per message, and use gauges for concurrent-stream counts.
- **A plugin timing core's work double-measures by design.** See :ref:`plugin_telemetry_callbacks` above.
Capstone review fixes: per-test reset, rename, provider guard, UpDownCounter, naming rules, privacy walk Outcome of a whole-stack review with the kit visible as one system: - otel_reset: an autouse fixture draining the span exporter and metric reader after every test. Without it a large suite accumulates hundreds of thousands of recorded spans in the session-scoped exporter - the likeliest amplifier of the slow-runner CI flakes - and plugins would inherit the same leak. - assert_registry_covered renamed to assert_spans_covered: the old name read as covering the whole registry, which is exactly wrong next to assert_metrics_covered. Public API is forever; renamed before anything ships, no alias. - The installers now verify their provider actually took: with a provider installed first (opentelemetry-instrument, an embedding app), set_*_provider() is silently ignored, and fixtures would assert against an exporter wired to nothing. They skip clearly instead. - UPDOWN_COUNTER registry kind, mapped to Sum with monotonicity checked both ways - a Counter must collect monotonic, an UpDownCounter must not. Previously an UpDownCounter's kind check was silently skipped. - The docs page now prescribes naming: scope = import package name (underscores), signal prefix = a name you own, never bare datasette.*; its own examples no longer teach the hyphenated outlier. Plus an observable-gauges pattern section and a prefix-overlap note. - assert_no_forbidden_values(): the enforcement half of the privacy rules - plant sentinel secrets in a workload and assert they never appear in any span name, attribute, event, status description or metric attribute, across all scopes by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
2026-09-02 14:26:45 -07:00
- ``datasette.client`` requests made from inside a request produce a nested ``SERVER`` span. Those spans carry ``datasette.internal_client: true`` - filter on it to keep kind-based dashboards from double-counting requests.