2026-09-24 09:11:08 -07:00
|
|
|
"""
|
|
|
|
|
Render the span reference in ``internals.rst`` from
|
|
|
|
|
``datasette/telemetry_registry.py``.
|
|
|
|
|
|
|
|
|
|
Driven by cog, and ``cog --check docs/*.rst`` runs in CI - so adding a span
|
|
|
|
|
without documenting it, or documenting one that no longer exists, is a build
|
|
|
|
|
failure rather than something a reader discovers later.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _attribute_lines(cog, attributes):
|
|
|
|
|
if not attributes:
|
|
|
|
|
cog.out(" No attributes.\n\n")
|
|
|
|
|
return
|
|
|
|
|
cog.out(" Attributes:\n\n")
|
|
|
|
|
for attribute in attributes:
|
|
|
|
|
suffix = " *(optional)*" if attribute.optional else ""
|
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
|
|
|
line = f" - ``{attribute}``{suffix} - {attribute.description}"
|
|
|
|
|
if attribute.values is not None:
|
|
|
|
|
rendered = ", ".join(f"``{value}``" for value in sorted(attribute.values))
|
|
|
|
|
line += f" One of: {rendered}."
|
|
|
|
|
cog.out(line + "\n")
|
2026-09-24 09:11:08 -07:00
|
|
|
cog.out("\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def spans(cog):
|
|
|
|
|
from opentelemetry.trace import SpanKind
|
|
|
|
|
|
|
|
|
|
from datasette.telemetry_registry import SPANS
|
|
|
|
|
|
|
|
|
|
cog.out("\n")
|
|
|
|
|
for span in SPANS:
|
|
|
|
|
cog.out(f"``{span}``\n")
|
|
|
|
|
cog.out(f" {span.description}\n\n")
|
|
|
|
|
# INTERNAL is the default and the overwhelming majority of spans -
|
|
|
|
|
# printing it on every one would be noise. Only the exceptional case,
|
|
|
|
|
# a real database call, is worth calling out.
|
|
|
|
|
if span.kind != SpanKind.INTERNAL:
|
|
|
|
|
cog.out(f" Kind: ``{span.kind.name}``.\n\n")
|
|
|
|
|
_attribute_lines(cog, span.attributes)
|
2026-09-01 12:24:01 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def metrics(cog):
|
|
|
|
|
from datasette.telemetry_registry import METRICS
|
|
|
|
|
|
|
|
|
|
cog.out("\n")
|
|
|
|
|
for metric in METRICS:
|
|
|
|
|
cog.out(f"``{metric}``\n")
|
|
|
|
|
cog.out(f" {metric.kind}, unit ``{metric.unit}``. {metric.description}\n\n")
|
|
|
|
|
if metric.buckets:
|
|
|
|
|
boundaries = ", ".join(f"``{boundary}``" for boundary in metric.buckets)
|
|
|
|
|
cog.out(f" Bucket boundaries: {boundaries}.\n\n")
|
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
|
|
|
_attribute_lines(cog, metric.attributes)
|