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
This commit is contained in:
Alex Garcia 2026-09-02 12:55:47 -07:00
commit 2a05263ab8
8 changed files with 304 additions and 59 deletions

View file

@ -50,10 +50,4 @@ def metrics(cog):
if metric.buckets:
boundaries = ", ".join(f"``{boundary}``" for boundary in metric.buckets)
cog.out(f" Bucket boundaries: {boundaries}.\n\n")
if metric.attributes:
cog.out(" Attributes:\n\n")
for attribute in metric.attributes:
cog.out(f" - ``{attribute}`` - {attribute.description}\n")
cog.out("\n")
else:
cog.out(" No attributes.\n\n")
_attribute_lines(cog, metric.attributes)