Plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs - #2899
Open
asg017 wants to merge 4 commits into
Open
Plugin telemetry kit: public registry API, linked_root_span_kwargs, test helpers, docs#2899asg017 wants to merge 4 commits into
asg017 wants to merge 4 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## asg017/otel-phase1-5-metrics #2899 +/- ##
=============================================================
Coverage 0.00% 0.00%
=============================================================
Files 75 76 +1
Lines 12738 12975 +237
=============================================================
- Misses 12738 12975 +237 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…rgs, 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
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
test_kit_module_itself_never_imports_the_sdk shells out, and like every subprocess-spawning test in this suite it crashes the interpreter with SIGBUS on macOS/CPython 3.13 when it runs late enough that the process holds many threads - the exact failure conftest already front-loads test_datasette_package_never_imports_the_sdk for. Move it to the front too, and note the hazard in assert_package_never_imports_sdk's docstring since plugin suites will call it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA
…Counter, 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
asg017
force-pushed
the
asg017/otel-phase1-6-plugin-kit
branch
from
September 2, 2026 21:28
0dcb712 to
71d382b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Claude-generated PR description
Fourth PR in the OTel stack, on top of #2897. Makes core's telemetry machinery importable by plugins instead of copyable.
Why
A survey of five plugin OTel plans (datasette-paper, datasette-agent, datasette-litestream, datasette-accounts, datasette-cron) found every single one hand-copying the same core machinery: the
Attribute/SpanName/MetricNameregistry classes (~60-80 lines each), the two-way conformance-test harness, theotel_spans/otel_metricspytest fixtures, the never-imports-the-SDK test, the bucket boundaries, and the detached-root-with-Linkrecipe. Two plugins had independently invented the same extensions (closed-enum attribute values; prefix-matched span families). Nobody was blocked — the patterns are good enough that everyone copies them verbatim — which is exactly the argument for making them API.What this adds
Public registry API (
datasette/telemetry_registry.py):Attribute(values={...})declares a closed enum, enforced by the conformance helpers — the enforceable version of "bounded, safe as a metric dimension". Enum values render in the generated docs.SpanName(prefix=True)registers a span family whose emitted names share a fixed prefix (chat {model}), matched byspan_for()after exact names. (This flag existed once and was removed as dead code — the plugin registries are the consumer that makes it real, so it returns with tests and a user.)span_for()/attribute_value_allowed()accept aspans=tuple so they work against plugin registries.linked_root_span_kwargs()(datasette/telemetry.py): the root-span-with-Linkshape for work a request caused without containing — background jobs, scheduled ticks,block=Falsewrites. Three of the five surveyed plugins planned to hand-roll exactly this incantation. Core's write thread now uses it instead of building the kwargs inline (dogfood).datasette.telemetry_testing: the session-scoped provider fixtures,otel_spans/otel_metrics, two-way conformance checkers for both spans and metrics, andassert_package_never_imports_sdk(). Imports the SDK lazily — fixtures skip when it's absent. Core's own conftest and conformance tests now import these instead of defining them, so the suite consumes the kit exactly the way a plugin's would.assert_spans_conform/assert_spans_covered— enum- and prefix-aware, filtered by instrumentation scope so a plugin is only judged against its own signals.assert_no_forbidden_values(): the enforcement half of the docs page's 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 (upstreamed from the datasette-accounts plan, which invented it).otel_reset: an autouse fixture draining the exporter and reader after every test, so a large suite doesn't accumulate recorded spans for its whole lifetime — and the installers now detect a pre-existing provider (opentelemetry-instrument, an embedding app) and skip clearly instead of asserting against an exporter wired to nothing.metric_for()+ a scope-retainingMetricsCollector):assert_metrics_conformadditionally verifies each instrument was created as the kind and unit its registry entry declares — drift between the registry entry and themeter.create_*()call was previously caught by nothing, in core or any plugin — and enforcesvalues=enums on metric dimensions, the place a cardinality bound matters most (datasette.operationnow declaresvalues={"read", "write"}as the dogfood). Four instrument kinds are supported includingUPDOWN_COUNTER, with monotonicity checked both ways.assert_metrics_coveredis the reverse direction; both*_coveredhelpers exemptoptional=Trueattributes so a workload isn't forced to manufacture every error path.Docs: new page :ref:
plugin_telemetry— "Telemetry for plugin authors": own-scope discipline, registry usage with a worked example, privacy/cardinality rules, pass-named-callables guidance,request_span()(now documented public API), the background convention (root span per tick with an outcome attribute, always emitted, linked not parented),ProxyTracer-vs-_ProxyMeterordering facts, prescribed naming rules (scope = import package name; signal prefixes you own, never baredatasette.*), an observable-gauges pattern section, and known caveats (streaming spans, deliberate double-measurement,datasette.clientsub-request spans — now marked withdatasette.internal_client: trueby #2863 so dashboards can filter them).Tests
tests/test_telemetry_testing_kit.py: a toy plugin registry + tracer + meter exercising both conformance directions for spans and metrics, enum enforcement, prefix families, kind/unit mismatch detection, optional-attribute exemption, scope filtering,linked_root_span_kwargs()(root, not child; link present; no-current-span case), and the kit's own lazy-SDK guarantee.tests/test_telemetry_registry.py: prefix lookup (restored), exact-beats-prefix precedence, enum value enforcement; the generic metric conformance tests are now kit-helper calls withscope_name="datasette", while the stricter literal-pinning and optional-coverage tests stay hand-written on purpose.🤖 Generated with Claude Code
https://claude.ai/code/session_012U7coQfVu8nK2R4q2mCULA