feat(metrics): install-inventory gauges and OTLP export verification - #7300
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A real fleet-observability gap, closed with the existing process-gauges pattern, honest failure semantics, and every known limitation named rather than hidden. Suggestions
[DESIGN-REVIEWED] e18f40b |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe repo's own This is a real correctness defect, but the exception is caught — no crash, data loss, corruption, or security hole — so it does not meet the BLOCKING bar. It is advisory. One advisory finding: the knowledge-documents probe builds its SQLite URI by raw interpolation, misreporting healthy Windows installs as a broken probe. FINDING — src/kiro_crew/metrics/inventory_gauges.py:543 — [OPUS-REVIEWED] e18f40b Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All verification greps done. The findings survived or died as follows: First-Principles-Verdict: PASS Fleet-drift visibility with every rider declared, each symptom-level stopgap naming its cause (#7266), and one duplicated loop deleted along the way. What this change shipsIntent: let an operator see, over time, what each of their opted-in machines has configured — an ADDITION, declared as such.
The one structural asymmetry I checked and accepted: Subtractions
[FIRST-PRINCIPLES-REVIEWED] e18f40b |
4dbca13 to
d34a9b1
Compare
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Round 1 dispositions -
|
d34a9b1 to
9483c7c
Compare
Round 2 dispositions -
|
9483c7c to
fb825e0
Compare
Round 3 dispositions -
|
fb825e0 to
4f43d40
Compare
Round 4 dispositions -
|
4f43d40 to
1e90287
Compare
Round 5 disposition -
|
735caf0 to
b4c77f6
Compare
Nothing sampled what an install has configured. process_gauges answers "how is this process behaving"; adoption questions -- how many installs schedule crons, have any knowledge documents, run third-party MCP servers -- had no source, so they were only answerable by asking a user. Adds kirocrew.inventory.* as eight observable gauges built on the same contract as process_gauges: callbacks run only when a reader collects, registration happens on the live build path so the telemetry.enabled consent gate covers them, and a probe that cannot answer yields no observation rather than a fake zero. Cost is the binding constraint, since a callback runs per export interval PER reader. Every probe is O(1) in-memory, one small file parse, or TTL-cached; the two expensive ones (a recursive skills walk, a SQLite open) are cached 300s and say so. The knowledge probe opens SQLite read-only and only when the database already exists, because constructing a KnowledgeStore would create it. Privacy: MCP server names classify and are then discarded -- only first_party / third_party counts leave. Knowledge and lesson counts report the raw number rather than a magnitude band: these metrics only reach the collector the operator configured, about machines that operator already owns, so a band protects nobody while costing a series per band and rendering growth inside a band -- the drift the gauge exists for -- as a flat line. A backend can band a raw count at query time and cannot recover a count from a band. Three spec corrections found while building it: there is no memory.enabled flag (the embedding provider is coerced on load, so memory is structurally always on) and the gauge reports memory.migrated instead; telemetry.enabled is excluded from the toggle set because inside the consent gate it could only ever read 1; and the first-party MCP set reuses mcp_discovery._MANAGED_SERVER_NAMES rather than restating a list that would drift. Install-level facts are identical across the processes an install runs at once, so exactly one publishes them: the gateway claims the role and every callback checks the claim at collect time. The claim sits in run() at a point no branch guards, because nothing enforces the election at runtime and an unclaimed install is silent in both directions -- the callbacks return before probing, so probe.failures stays empty too and the family reads like a host that stopped exporting. A test parses the gateway and pins the call site. A present-but-unreadable crons.json now counts a probe failure instead of only yielding a gap: cron's own _read_job_records calls that case a fault, and a silent gap is indistinguishable from a host that stopped exporting, which is the one thing probe.failures exists to prevent. An ABSENT store stays a quiet 0 -- a fresh install has no crons. The enabled-count reduction moves to a new module-level cron.enabled_count_from_disk returning (count, loadable), which CronService.count_enabled_from_disk now calls too, so the two readers share one loop instead of two spellings of it. Also adds the verification the metrics module never had. test_local_exporter.py already proves serialization, but of the exporter alone -- built directly and fed from a provider assembled in the test. Nothing asked the same question of the LIVE build, where an instrument can be missing outright and an attribute value or temporality can be right in memory and wrong on the wire. test_otlp_wire_e2e.py drives the real build through the real exporter and asserts the roster, resource-attribute fidelity (compared against the provider's own resource, so a future attribute is covered the moment it lands), the closed attribute enums, and temporality; a second tier posts through a real OTLPMetricExporter to an in-process loopback receiver and decodes the protobuf, skipped unless the optional extra is installed. Driving a real collector needs a downloaded binary and network, so it stays a manual check: the operator guide describes it against whatever collector you already run, which survives a version bump in a way a pinned script does not. Refs #7232 #7257
b4c77f6 to
e18f40b
Compare
Round 9 dispositions (head
|
test_irq.py measured its 10ms coalescing floor in real wall clock: two assertions that a floor has NOT yet expired had only the gap between two _verdict() calls as budget, so any >=10ms scheduling stall on a loaded CI runner let the floor close and the assertion fail (observed on PR #7300, Backend Tests (3.12, 2)). An autouse fixture now installs a fake clock over the time name as kiro_crew.irq resolves it (never the stdlib module object), _settle() advances that clock instead of sleeping, and the three pre-aged state stamps derive from it. Intervals the kernel measures are now exact by construction: the clock only moves when the test moves it, so no stall can age a window between two calls. Mutation-verified: reverting the #7431 behaviour (joining entry inherits the window's age) makes test_an_entry_joining_after_a_partial_fire_serves_its_own_floor fail under the fake clock, so the converted test still guards the defect. Closes #7598
test_irq.py measured its 10ms coalescing floor in real wall clock: two assertions that a floor has NOT yet expired had only the gap between two _verdict() calls as budget, so any >=10ms scheduling stall on a loaded CI runner let the floor close and the assertion fail (observed on PR #7300, Backend Tests (3.12, 2)). An autouse fixture now installs a fake clock over the time name as kiro_crew.irq resolves it (never the stdlib module object), _settle() advances that clock instead of sleeping, and the three pre-aged state stamps derive from it. Intervals the kernel measures are now exact by construction: the clock only moves when the test moves it, so no stall can age a window between two calls. Mutation-verified: reverting the #7431 behaviour (joining entry inherits the window's age) makes test_an_entry_joining_after_a_partial_fire_serves_its_own_floor fail under the fake clock, so the converted test still guards the defect. Closes #7598
test_irq.py measured its 10ms coalescing floor in real wall clock: two assertions that a floor has NOT yet expired had only the gap between two _verdict() calls as budget, so any >=10ms scheduling stall on a loaded CI runner let the floor close and the assertion fail (observed on PR #7300, Backend Tests (3.12, 2)). An autouse fixture now installs a fake clock over the time name as kiro_crew.irq resolves it (never the stdlib module object), _settle() advances that clock instead of sleeping, and the three pre-aged state stamps derive from it. Intervals the kernel measures are now exact by construction: the clock only moves when the test moves it, so no stall can age a window between two calls. Mutation-verified: reverting the #7431 behaviour (joining entry inherits the window's age) makes test_an_entry_joining_after_a_partial_fire_serves_its_own_floor fail under the fake clock, so the converted test still guards the defect. Closes #7598
buluoray
left a comment
There was a problem hiding this comment.
Approved — 0 blocking, 3 non-blocking findings
Independent review of e18f40bf5, verified against source in a throwaway worktree rather than from the description. Every new execution path here is fail-closed: a probe that cannot answer yields a missing series plus a counted probe.failures increment, which is the exact confusion that counter exists to resolve. Nothing crashes, nothing exports a wrong number, nothing can break an export cycle. Nothing found justifies another blocking round.
The two things most likely to be quietly wrong were both right
Observable-counter monotonicity. kirocrew.inventory.probe.failures must report a monotonic cumulative total. The specific hazard I went looking for — a TTL-cached None re-incrementing on every collection, so one stuck probe inflates the count with no new failure and misreports the rate — does not occur: _ttl_cached returns a cache hit without reaching produce() or _note_probe_failure, and _probe_failures is increment-only outside the test reset. Corroborated by test_ttl_cache_caches_a_raised_failure_as_a_gap.
Concurrency under the PR's own stated two-reader condition. _lock is a real threading.Lock; every _cache access is guarded and the stored value is an atomic (expiry, value) tuple, while produce() deliberately runs outside the lock. Worst concurrent outcome on a cold entry is a double-produce (two SQLite opens), still monotonic — no torn read, no lost failure record, no deadlock. The auto-nudge claim also holds: list(service._loops.values()) is a single C-level construction that cannot observe a mid-iteration mutation, and _loops is mutated only on the loop thread.
Mutation-verified: 8 of 9 ratchets are genuinely falsifiable
Each mutation was byte-verified as landed before running pytest, to rule out no-op false greens.
| Claim | Mutation | Result |
|---|---|---|
| Call-site ratchet | wrap in if; move to another method; call twice; delete |
all four reddened test_reporter_claim_is_unconditional |
| Privacy ratchet | emit MCP server name as an attribute value | reddened at both the reader and the wire |
| Drift (knowledge) | wrong KNOWLEDGE_SOURCES_TABLE |
reddened |
| Drift (MCP) | restate a managed server name locally | reddened |
| Anti-typo | rename a declared toggle to a nonexistent field | reddened |
| Series-shape | band a store-backed count (constant 1 + attribute) | reddened both halves |
| Instrument roster | unregister one gauge | reddened |
| Resource fidelity | drop service.name only on the wire |
reddened |
| Write refusal | mode=ro → mode=rwc |
reddened |
The resource-fidelity assertion is genuinely dynamic, not a hardcoded list in disguise: I dropped the attribute on the serialization side only, leaving the live provider's Resource intact, and it caught it. A construction-side drop would have escaped precisely because both sides move together — which confirms the comparison is against the provider's own resource.
Also verified: the cron.py extraction is line-for-line behavior-preserving with the loadable half genuinely guarded (forcing (count, True) reddened two tests); enabled_count_from_disk has exactly the two claimed consumers; no code path registers inventory gauges while telemetry.enabled is off; the reporter claim sits at unguarded method-body indentation in run() after the _test_mode block closes; instrument parity between code and spec is exact (9 process + 9 inventory); docs-lint.sh passes; cron suite 2244 passed, metrics suite 606 passed.
Findings — all non-blocking, all in prose that describes code
1. The list_jobs timer rationale is factually wrong, and doubly unreachable. read_active_crons's docstring (src/kiro_crew/metrics/inventory_gauges.py:433-436) justifies reading from disk because "list_jobs re-arms the asyncio timer — calling that from a ticker thread with no running loop would raise, and worse, it cancels the existing timer first, which would stop every scheduled job."
Neither half survives current source. list_jobs (src/kiro_crew/cron.py:2901-2919) is cache-only — its body is return self._snapshot(list(self._jobs), include_disabled) and its own docstring says it performs no filesystem I/O; the _sync() → _load() → _arm_timer() path belongs to list_jobs_async. And _arm_timer (src/kiro_crew/cron.py:3081-3090) was already hardened for exactly this case: off-loop it takes except RuntimeError → loop = None → call_soon_threadsafe → return, cancelling nothing and raising nothing. Its inline comment names the blind-cancel hazard as the reason it was fixed.
This text is inherited, not invented — the same claim sits verbatim on main in count_enabled_from_disk's docstring, which this PR leaves untouched. What the PR does is propagate a condensed copy into a second file. The decision to read from disk stays correct for its other stated reasons (no process-global CronService handle, loadable is needed, drift avoidance). Worth fixing because this module's whole thesis is not trusting an inference that silently stops being true, and it now carries one about a neighbouring module. Ideally correct both sites.
2. The spec's pod paragraph is overstated for one of the nine gauges. docs/system-specs/modules/metrics.md:389-391 says a pod "boots with its own KIROCREW_HOME and copies no crons, sessions, or databases, so its inventory belongs to a different install and publishing it is correct." True for the stores it names — but mcp_discovery._mcp_sources() (src/kiro_crew/mcp_discovery.py:296-299) returns data_home()/"mcp.json" and Path.home()/".kiro"/"settings"/"mcp.json", and the second is machine-global. So host and pod publish overlapping readings for kirocrew.inventory.mcp.servers.
Not introduced here: agent.py:316, apps/bridges.py:216 and dashboard/handlers/mcp.py:86 all resolve that same host path, and kiro_home()'s own docstring names settings/mcp.json as a reader still pinned to host ~/.kiro. One qualifying sentence, no code change. Given how carefully the two-gateway limitation is documented in three places, this reads like an oversight rather than a position.
3. test_knowledge_probe_never_creates_the_database is fixture-backstopped, not probe-guarded. I could not falsify it by source mutation: even after removing the db_path.exists() short-circuit and switching to mode=rwc, it stayed green — sqlite3.connect will not create parent directories, and the fixture's tmp_path has no workspace/knowledge/. Its sibling write-refusal assertion is cleanly pinned, so the read-only mode is genuinely load-bearing; it is only this one assertion that cannot fail for the reason it exists. Making it falsifiable needs the fixture to pre-create the parent directory.
Two findings I would retire
Opus 4.8's Windows SQLite URI advisory does not hold on its stated mechanism. The probe's path is config_dir()/workspace/knowledge/knowledge.db (inventory_gauges.py:534) with fixed trailing literals, so no ? or # can appear — and ?/# truncation is precisely what the repo's pathname2url/as_uri() convention defends against (snapshot.py:611-617 documents it in those terms). Every cited precedent opens a user- or project-controlled path where the escape is load-bearing; this one does not. Failure would be fully degraded anyway (_ttl_cached catches, records a knowledge failure, caches None). At most a defense-in-depth consistency nit.
timeout=2.0 bounds lock-wait rather than query execution — fine for a COUNT(*) on the sources table, not worth a change.
Approving. The three findings are follow-up material: none is fail-open, two are pre-existing or inherited, and after nine bot rounds the only class left is cross-references to untouched files — which is the class the automated lanes structurally cannot see.
Problem / Motivation
Nothing samples what an install has actually configured.
process_gaugesanswers"how is this process behaving" -- threads, file descriptors, RSS, GC -- but an
operator running Kiro Crew on more than one machine has no way to see that a host
stopped scheduling crons, that skills and knowledge documents are piling up on one
box and absent on another, or that a feature switch differs between hosts. The
dashboard shows one machine's counts on request; nothing samples them over time, so
config drift between hosts is only visible by opening each dashboard in turn.
Scope, stated up front because the metric names invite the opposite reading:
this is operator-fleet telemetry, not project-wide adoption analytics. Collection is
off by default, egress is a second opt-in, and OTLP needs the
kirocrew[otlp]extra, so any aggregate over these gauges describes one operator's own opted-in
machines. Install analytics structurally cannot ride this trunk --
beacon.pyisthat channel, and its docstring plus the spec's "Why it is NOT part of the OTEL
trunk" section give four independently disqualifying reasons. That boundary is now
written into the module docstring, the spec, and the operator guide so it is not
re-litigated later.
What this answers today, and what it does not. Hosts are separable at a moment:
service.instance.idcomes from the SDK's default resource, and the version pinnedfor the
kirocrew[otlp]extra (1.44.0) supplies it. What is NOT available yet is thelongitudinal per-host view, because that id is regenerated per process, so a host's
series restarts with its gateway. The fix is a persisted install-scoped resource
identity and it is already in flight as #7266, which makes
service.instance.idthepersisted
beacon.install_id. Nothing here needs to change when it lands: theresource assertions in this PR compare against the provider's own resource rather
than a fixed list, so new attributes are covered the moment they appear. That work
is also the precondition for ever removing the reporter election below, though not a
drop-in replacement for it: a stable install id makes downstream dedup possible
(
max by (install)), but that is query-time discipline -- the data would still carryone copy per process, and a naively written fleet
sumwould read N times the truth,where the election makes it correct at the point of emit. Until then
these gauges are honest as fleet aggregates and as point-in-time per-instance
readings, and the operator guide says so where an operator will hit it.
There is a second, narrower gap.
test/metrics/test_local_exporter.pyalready provesserialization, but it proves it of the exporter alone: built directly and fed from a
provider assembled in the test. Nothing asked the same question of the live build
-- that the roster
provider._build_recorder()registers actually arrives, carryingthe attribute values the modules declare, at the temporality each instrument kind
requires. An instrument can be missing outright, and an attribute value or a
temporality setting can be correct in the SDK's in-memory view and still be dropped
or wrong by the time it leaves the process, and nothing would catch it.
Why it matters
An operator cannot currently distinguish a host that is idle from one whose cron
store stopped loading, or notice that one machine's skills tree drifted, without
checking each dashboard by hand. These are exactly the questions a time series
answers cheaply and a point-in-time panel cannot.
Without a serialization test, the failure mode is silent and lands on operators:
metrics appear to work locally, then arrive at a backend missing a label or with
counters that look like they reset constantly. That is the class of defect that
survives to production because the in-memory assertion passed.
What changed (motivation -> approach -> change)
Inventory gauges.
src/kiro_crew/metrics/inventory_gauges.pyadds eightobservable gauges under
kirocrew.inventory.*, mirroringprocess_gaugesdeliberately rather than inventing a second pattern: callbacks run only when a
reader collects, registration happens on
_build_recorder's live path so thetelemetry.enabledconsent gate covers them, raw readers stay SDK-free so they areunit-testable without a pipeline, and a probe that cannot answer yields no
observation rather than a fake zero.
Cost drove most of the design. A callback runs once per export interval per
reader, so an install with an OTLP destination configured enters these on two
ticker threads concurrently. Every probe is O(1) in-memory, one small file parse, or
explicitly TTL-cached, and the module documents the cost class of each:
crons.jsonthrough cron's own record helpers -- the same onescount_enabled_from_diskuses, so it cannot drift from what the schedulerconsiders enabled. Deliberately not
list_jobs, which re-arms the asyncio timerand from a reader thread would raise after cancelling the live timer, stopping
every scheduled job.
SQLite open) and are cached 300s. The knowledge probe opens SQLite read-only
and only when the database already exists: constructing a
KnowledgeStorerunsschema init plus graph load and would create the database on an install that
never ingested anything.
safe from a foreign thread even though the registry's
asyncio.Lockis not.Privacy shaped one choice, and rejected a second. MCP server names are read to
classify and then discarded -- only
first_party/third_partycounts leave,because a roster is user-chosen text and would be both unbounded cardinality and a
disclosure of what the user has installed. Knowledge and lesson counts, by contrast,
publish the raw number: banding them was the tempting move on the theory that an
exact count is a slowly-changing fingerprint, but that argument does not survive the
scope above. These metrics only reach the collector the operator configured, about
machines that operator already owns and can identify by far more than a document
count -- so the band protects nobody, while it costs a series per band, renders
growth inside a band (the drift the gauge exists for) as a flat line, and fixes the
boundaries at emit time. A backend can band a raw count at query time; it cannot
recover a count from a band. The product's own thresholds (50 =
knowledge.max_sourcesdefault, 200 = lesson prune ceiling) are named in the operator guide instead.
One publisher per install. Install-level facts are identical across the several
processes an install runs at once -- gateway, MCP gateway daemon, spawned agents -- so
publishing them from each would count one install once per process: a fleet sum reads
N times the truth, and a fleet average is dragged toward whichever installs happen to
run the most processes.
Deduplicating downstream needs an install-scoped resource identity that does not exist
yet, so exactly one process publishes: the gateway calls
mark_install_reporter(), andevery callback checks the claim at COLLECT time rather than at registration, so a
recorder built before the claim publishes nothing until it lands instead of publishing
wrongly.
Its placement is part of that contract, not an incidental detail. Nothing enforces the
election at runtime, and an unclaimed install is silent in both directions -- the
callbacks return before probing, so even
probe.failuresstays empty and the familyreads exactly like a host that stopped exporting. So the claim sits in
GatewayOrchestrator.run()at a point no branch guards, andtest_reporter_claim_is_unconditionalparses the gateway to assert that: calledexactly once, from
run(), without passing through a conditional (tryis allowed --telemetry must never block boot).
Three points where the intended design did not survive contact with the code, all
resolved toward reporting something true rather than something convenient:
memory.enabledflag. The embedding provider is coerced to areal value on load, so memory is structurally always on and an "enabled" gauge
could only ever read 1. The gauge reports
memory.migratedinstead, which is areal question (how far the vector-store migration has reached).
telemetry.enabledis excluded from the toggle set: this module only runsinside the consent gate, so it is a tautology dressed as a measurement.
mcp_discovery._MANAGED_SERVER_NAMESrather thanrestating a name list that would drift silently the first time a managed server
is renamed.
Verification.
test/metrics/test_otlp_wire_e2e.pycloses the serialization gapin two tiers. Tier 1 drives the real
_build_recorderlive path through its realexporter and asserts the instrument roster, resource-attribute fidelity, the
closed attribute enums, and temporality. Fidelity is asserted by comparing the
serialized resource against the live provider's own resource rather than a hardcoded
list, so an attribute added to the resource later is covered the moment it lands and
one the exporter silently drops fails here. Tier 2 posts through a real
OTLPMetricExporterto an in-process receiver bound to loopback and decodes theprotobuf; it skips unless the optional
kirocrew[otlp]extra is installed.The two tiers together also pin a boundary neither could pin alone:
kirocrew.process.start_timeis a host-local, reboot-unique token the JSONLexporter stamps per record, so tier 1 asserts it IS on the local shard and tier 2
asserts it is absent from the OTLP payload. Either assertion alone passes while the
attribute sits on the wrong side of the boundary.
Those two tiers are the whole automated surface, and they stop at the process
boundary. Driving a real collector needs a downloaded binary and outbound network, so
it cannot be a test in a network-isolated CI, and this PR does not ship a script for
it either: the operator guide describes how to do that check by hand against whatever
collector you already run, which is the form that survives a version bump. See the
Tests section for the one-time run that was done during development, and what it is
and is not evidence of.
Production wiring outside the new module is three hunks.
provider.pyregisters theinventory gauges next to the process ones, in its own
tryblock so a failure ineither family cannot cost the other -- they read entirely different subsystems.
slack/gateway.pyclaims the reporter role, as above. Andcron.pygainsenabled_count_from_disk(path) -> (count, loadable), a module-level sibling ofunhealthy_jobs_from_diskthat becomes the single owner of the enabled-countreduction:
CronService.count_enabled_from_disknow calls it and keeps the count,this probe calls it and needs
loadabletoo. Both sides previously carried their ownspelling of that loop.
Docs:
docs/guides/telemetry-otlp-export.mdcovers pointing the exporter at acollector and on to CloudWatch, Datadog, or any OTLP-compatible backend, with
collector config samples (explicitly marked illustrative and not tracked, since
vendor options drift with releases this repo does not follow), the two separate
consent switches, what is and is not exported, and
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE-- the setting that decideswhether a backend accepts the data, which works precisely because the reader builder
passes no explicit temporality.
docs/system-specs/modules/metrics.mdgains a rowper new instrument, so the spec moves with the code.
Tests
test/metrics/test_inventory_gauges.py(43 cases), four layers matching themodule:
pinned. The split is not just none-vs-zero but SILENT-vs-COUNTED: an absent source
is a quiet gap because nothing is broken (no auto-nudge service in this process, a
knowledge database on an install that never ingested, a missing cron store on a
fresh install), while a source that is PRESENT and unreadable is a fault and
increments
probe.failuresbefore returning None. An unparseablecrons.jsonisthe case in point -- cron's own
_read_job_recordscalls it a fault, and a silentgap there would be indistinguishable from a host that stopped exporting, which is
the confusion the counter exists to resolve. Pinned at the reader level and again
through a real collection.
after expiry, and caches a
Noneanswer so an install that can never answer doesnot re-pay an expensive probe every cycle.
read-only connection must refuse a write.
test_every_declared_toggle_resolves_against_a_real_configasserts every declared toggle resolves against a real config. The runtime omits an
unresolvable key (a renamed field must read as a missing series, never as a switch
someone turned off), which is right but would hide a typo forever; this is what
makes the omission observable.
the constant stays the single owner.
attribute key or value.
attribute-free series carrying the pinned reading, at the reader level and again on
the wire -- a reintroduced band fails both halves (constant 1, plus an attribute).
test_reporter_claim_is_unconditionalparses the gateway andasserts
mark_install_reporter()is called exactly once, fromrun(), withoutpassing through a conditional -- the one invariant that has no runtime enforcement
and no failure signal when it breaks.
other metric survives; registration never raises even on a hostile meter; and an
inventory registration failure does not cost the process gauges.
test/metrics/test_otlp_wire_e2e.py(14 cases) as described above, including theprivacy ratchet repeated at the serialization boundary -- the reader-level test
proves names are discarded, this proves nothing downstream puts them back.
Both e2e tiers pin their probe readings rather than reading host state, so the
assertions are about the serialization contract and cannot flake on whether the
machine running them happens to have a knowledge database or any crons.
Manual verification
pytest test/metrics/ -qrun three times: 549 passed each time.scripts/check_black_formatting.pypasses with the changed files in scope, andscripts/docs-lint.shpasses.80% floor.
it), so the skipped-in-CI tier is not untested code.
otelcolwas driven end to end ONCE during development -- checksum verified,collector started, one export pushed through it, 16 instruments confirmed received.
That is a one-time result, not a gate: nothing in this PR re-runs it, and it does not
protect against a future regression. It is reported here because it is the only
evidence that a real collector accepts this payload; the repeatable half of that
check is tier 2, which decodes the real exporter's protobuf in CI.
under
--disable-nosemand is silent without it), rather than by re-running CI.Related Issues
Refs #7232 #7257