Skip to content

Commit 513e7e1

Browse files
wshallwshallclaude
andcommitted
feat(ops): publish ADR 0114's degraded gauge on /status, /metrics and the console
AC-7 requires "a WARNING naming the reason + degraded gauge", and §4's compensating-control story assumes an operator can SEE the degraded state. Nobody could: claim_proc_effective / claim_proc_degraded_reason were read by the store's own tests and nothing else — no /stats, no /status, no /metrics, no console. The whole operator signal was one WARNING at open(), in a log nobody was watching, that until PR #86 named the wrong cause. That is a load-bearing part of why the lever could sit inert in every deployment for its entire life. Surfaces the store's claim_proc_status() on: * GET /status -> claim_proc: effective, the human-readable degraded_reason, and the matched head_forms * GET /metrics -> messagefoundry_store_claim_proc_effective and messagefoundry_store_claim_proc_head_verbatim * /ui/status -> store-panel rows Three shape decisions. The field/series are ABSENT, not 0/false, when the lever was never requested: a constant 0 on every SQLite fleet is unalertable noise, and absence keeps "not requested" distinct from "requested and degraded". No reason LABEL in the exposition — the reason is free text embedding a proc name and, on the probe-failure arm, an exception string, so a label would be unbounded cardinality and a breach of the exporter's strict {connection, destination, status, version, le} allowlist; the string goes to /status and the console. And it deliberately does NOT feed the console's engine-health heart: claims keep flowing on the batch, so a degrade is a lever not paying off, not an unwell store, and making the nav cry wolf would devalue the signal that means it is. head_forms is surfaced for the same reason it is logged: a fleet reporting "verbatim" is a live counterexample to _CLAIM_PROC_STORED_HEADS's compatibility assumption, and was previously visible only at INFO. Observability only — the gate's accept/degrade logic is untouched. The new tests assert the rendered output of each surface, not the existence of a property. Rendering a new SystemStatus field on the console is a seam change, so ENGINE_UI_SEAM and the console's SUPPORTED_ENGINE_SEAMS both move to 16 and the golden snapshot is refreshed. A separate seam rather than a correction to the unreleased v15: v15 is a SecurityPosture change, and folding an unrelated DTO into it would make that note describe a field set it does not cover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 151cf41 commit 513e7e1

8 files changed

Lines changed: 365 additions & 5 deletions

File tree

messagefoundry/api/_ui_seam.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@
7575
#: so an older console simply ignores it; bumped rather than corrected in place because v14 SHIPPED
7676
#: (v0.3.2). Under "one shipped posture, loosen only" a subset that reads as the whole posture is the
7777
#: failure this field exists to prevent, so the console must be able to render the caveat.
78-
ENGINE_UI_SEAM: int = 15
78+
#: seam v16: SystemStatus gained the additive `claim_proc` — ADR 0114 AC-7's degraded gauge (whether the
79+
#: SQL Server stored-procedure claim path passed its startup gate, and the reason string when it did
80+
#: not), which the status page's store panel renders. Additive with a default and `None` on every
81+
#: backend without the lever, so an older console simply ignores it; a separate seam rather than a
82+
#: correction to v15 because v15 is a SecurityPosture change and folding an unrelated DTO into it would
83+
#: make that note describe a field set it does not cover.
84+
ENGINE_UI_SEAM: int = 16
7985

8086

8187
@dataclass(frozen=True, slots=True)

messagefoundry/api/app.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
CapturedResponseInfo,
8484
ChannelInfo,
8585
ClaimPoolInfo,
86+
ClaimProcInfo,
8687
ClusterNode,
8788
ClusterNodeList,
8889
ClusterStatus,
@@ -4477,6 +4478,21 @@ async def system_status(
44774478
if pool_status is not None
44784479
else None
44794480
)
4481+
# ADR 0114 AC-7's degraded gauge. Until this field existed the ONLY signal that the proc
4482+
# claim path had fallen back to the shipped batch was a WARNING at store open — which is a
4483+
# load-bearing part of why the gate could degrade in every deployment unnoticed. None unless
4484+
# [store].fifo_claim_proc is on and the backend has the lever, so the payload is unchanged
4485+
# by default. Synchronous + free (attributes the gate recorded once at open).
4486+
cps = engine.store.claim_proc_status()
4487+
claim_proc = (
4488+
ClaimProcInfo(
4489+
effective=cps.effective,
4490+
degraded_reason=cps.degraded_reason,
4491+
head_forms=dict(cps.head_forms),
4492+
)
4493+
if cps is not None
4494+
else None
4495+
)
44804496
# App-log disk metering (#50), alongside the DB metrics — only when a log dir is configured.
44814497
# Run the blocking stat()s off the event loop (the DB metering is itself off-loop in the store);
44824498
# None when stdout-only or the directory is unreadable, so /status never raises on it.
@@ -4520,6 +4536,7 @@ async def system_status(
45204536
logs=logs,
45214537
update=update,
45224538
pool=pool,
4539+
claim_proc=claim_proc,
45234540
)
45244541

45254542
# --- runtime log verbosity + redacted log-tail viewer (BACKLOG #171, ADR 0130) ----

messagefoundry/api/metrics.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838

3939
from messagefoundry import __version__
4040
from messagefoundry.store.pool_metrics import PoolStatus
41-
from messagefoundry.store.store import DestinationMetrics, InboundMetrics, LatencyHistogram
41+
from messagefoundry.store.store import (
42+
ClaimProcStatus,
43+
DestinationMetrics,
44+
InboundMetrics,
45+
LatencyHistogram,
46+
)
4247

4348
if TYPE_CHECKING: # avoid pulling the heavy engine import into the default path
4449
from messagefoundry.pipeline import Engine
@@ -192,6 +197,10 @@ class _Snapshot:
192197
pool: PoolStatus | None = None
193198
committed_txns: int = 0
194199
body_copies: int = 0
200+
# ADR 0114 AC-7's degraded gauge. None when the backend has no fifo_claim_proc lever or the flag
201+
# is off — the gauges are then ABSENT rather than 0, so a scrape can tell "not requested" from
202+
# "requested and degraded" (a constant 0 on every SQLite fleet would be pure alert noise).
203+
claim_proc: ClaimProcStatus | None = None
195204

196205

197206
async def gather_snapshot(engine: Engine) -> _Snapshot:
@@ -230,6 +239,7 @@ async def gather_snapshot(engine: Engine) -> _Snapshot:
230239
pool=pool,
231240
committed_txns=committed_txns,
232241
body_copies=body_copies,
242+
claim_proc=engine.store.claim_proc_status(),
233243
)
234244

235245

@@ -369,6 +379,33 @@ def collect(self) -> Iterable[Any]:
369379
body_copies.add_metric([], float(s.body_copies))
370380
yield body_copies
371381

382+
# ADR 0114 AC-7 degraded gauge. Emitted ONLY when [store].fifo_claim_proc is on: a constant
383+
# 0 on every fleet that never asked for the lever is noise a scraper cannot alert on, and
384+
# absence is the honest encoding of "not applicable here". Numeric and LABEL-LESS by
385+
# design — the human-readable degrade reason is free text (it embeds a proc name and, on the
386+
# probe-failure arm, an exception string), so carrying it as a label would both blow the
387+
# cardinality budget and break this module's strict {connection,destination,status,version,le}
388+
# allowlist. The reason string lives on /status and the console store panel instead.
389+
cp = s.claim_proc
390+
if cp is not None:
391+
effective = GaugeMetricFamily(
392+
"messagefoundry_store_claim_proc_effective",
393+
"1 when the ADR 0114 stored-procedure claim path passed its startup gate and is"
394+
" active, 0 when it degraded to the shipped ad-hoc batch (claims still flow).",
395+
)
396+
effective.add_metric([], 1.0 if cp.effective else 0.0)
397+
yield effective
398+
# Which stored head form the deployed modules matched. "verbatim" means this server did
399+
# NOT rewrite the CREATE OR ALTER head — no engine measured to date does, so a fleet
400+
# reporting 1 here is a live counterexample worth knowing about, not a fault.
401+
verbatim = GaugeMetricFamily(
402+
"messagefoundry_store_claim_proc_head_verbatim",
403+
"1 when at least one deployed claim procedure's stored definition kept the CREATE"
404+
" OR ALTER head verbatim (this server does not rewrite it), else 0.",
405+
)
406+
verbatim.add_metric([], 1.0 if "verbatim" in cp.head_forms.values() else 0.0)
407+
yield verbatim
408+
372409
# Connection-pool saturation + acquire-wait (server backends only; absent on SQLite, which has
373410
# no pool). [store].pool_size previously emitted NO saturation metric — these close that gap.
374411
pool = s.pool

messagefoundry/api/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,26 @@ class PoolInfo(BaseModel):
710710
claim_pool: ClaimPoolInfo | None = None
711711

712712

713+
class ClaimProcInfo(BaseModel):
714+
"""The ADR 0114 sub-lever A (``fifo_claim_proc``) startup-gate verdict — AC-7's **degraded
715+
gauge**, surfaced as the additive ``claim_proc`` field on :class:`SystemStatus`.
716+
717+
``None`` on every backend without the lever and on SQL Server when the flag is off, so "not
718+
requested" reads differently from "requested and degraded". When ``effective`` is False,
719+
``degraded_reason`` says why the store fell back to the shipped ad-hoc batch — claims keep
720+
flowing either way, so this is a performance-lever gauge, not a health alarm.
721+
722+
Metadata only: proc names, a head-form word, and the gate's own reason string — no message
723+
content and no PHI."""
724+
725+
effective: bool # the gate passed; pooled claims run through the procs
726+
degraded_reason: str | None = None # why it degraded to the batch; None when effective
727+
# proc name -> the stored head form the deployed module matched ("rewritten" | "verbatim").
728+
# "verbatim" means this server does NOT rewrite CREATE OR ALTER — no engine measured to date
729+
# does, so it is worth reporting; it is an engine difference, not a fault.
730+
head_forms: dict[str, str] = Field(default_factory=dict)
731+
732+
713733
class SystemStatus(BaseModel):
714734
engine: EngineInfo
715735
# Engine-wide top-line roll-up KPIs (#93): total messages, combined in+out connection count with
@@ -727,6 +747,10 @@ class SystemStatus(BaseModel):
727747
# percentiles + size/idle occupancy). Additive + ``None`` on SQLite (no pool) so the existing
728748
# payload is unchanged on the default backend and an older client deserializes /status unchanged.
729749
pool: PoolInfo | None = None
750+
# ADR 0114 AC-7's degraded gauge: whether the SQL Server proc claim path is effectively active,
751+
# and why not when it isn't. Additive + ``None`` on every backend without the lever and whenever
752+
# [store].fifo_claim_proc is off, so the default payload is unchanged.
753+
claim_proc: ClaimProcInfo | None = None
730754

731755

732756
class IntegrityResult(BaseModel):

messagefoundry_webconsole/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
# If cross-seam support is ever genuinely wanted, re-widen this set AND add the CI matrix that
4646
# installs the MIN and MAX supported engine builds — the claim and its test land together, or not
4747
# at all.
48-
SUPPORTED_ENGINE_SEAMS: frozenset[int] = frozenset({15})
48+
SUPPORTED_ENGINE_SEAMS: frozenset[int] = frozenset({16})
4949

5050
#: The vendored static assets shipped in THIS wheel (mounted at /ui/static by :func:`mount_ui`).
5151
STATIC_DIR = Path(__file__).parent / "static"

messagefoundry_webconsole/pages/monitoring.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,30 @@ def status(
352352
],
353353
adjustable=False,
354354
)
355+
# ADR 0114 AC-7's degraded gauge, on the panel an operator actually reads. Empty unless
356+
# [store].fifo_claim_proc is on (SQL Server only), so the default page is unchanged — no blank
357+
# row, no "—" that would read as a broken lever. A degrade is NOT an engine-health fault: claims
358+
# keep flowing on the shipped batch, so it stays a row here and deliberately does not feed the
359+
# nav heart, which would then cry wolf about a performance lever merely not paying off.
360+
# Metadata only: proc names, a head-form word, and the gate's own reason string.
361+
claim_proc_rows: list[list[object]] = []
362+
cp = sys.claim_proc
363+
if cp is not None:
364+
claim_proc_rows.append(
365+
[
366+
"Claim path (ADR 0114 stored procedures)",
367+
"active" if cp.effective else "DEGRADED — running the shipped batch",
368+
]
369+
)
370+
if not cp.effective:
371+
claim_proc_rows.append(["Claim path — why it degraded", _opt(cp.degraded_reason)])
372+
elif cp.head_forms:
373+
claim_proc_rows.append(
374+
[
375+
"Claim path — stored head forms",
376+
", ".join(f"{k}: {v}" for k, v in sorted(cp.head_forms.items())),
377+
]
378+
)
355379
store_tbl = rows_table(
356380
["Field", "Value"],
357381
[
@@ -415,6 +439,7 @@ def status(
415439
["Messages", db.messages],
416440
["Events", db.events],
417441
["Audit rows", db.audit],
442+
*claim_proc_rows,
418443
],
419444
adjustable=False,
420445
)

tests/golden/webconsole_seam.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# This is a GOLDEN gate: any diff means the seam contract changed - see the test's failure hint.
66

77
## ENGINE_UI_SEAM
8-
15
8+
16
99

1010
## dataclass messagefoundry.api._ui_seam.UiDeps
1111
engine_seam
@@ -170,7 +170,7 @@ SecurityPosture: allow_unencrypted_phi, backend, client_address_monoculture, cli
170170
ServiceStatusInfo: enabled, service_name, state
171171
StatsResetRequest: all, targets
172172
StatsResetTarget: channel_id, destination, role
173-
SystemStatus: db, engine, kpis, logs, pool, update
173+
SystemStatus: claim_proc, db, engine, kpis, logs, pool, update
174174

175175
## api.auth_models DTO fields rendered by the console
176176
AdGroupMap: entries

0 commit comments

Comments
 (0)