Skip to content

ops(0455): trustworthy deploy diff, dashboard coverage, guard fails closed - #427

Open
karolko9 wants to merge 14 commits into
masterfrom
ops/0455_observability-second-slice
Open

ops(0455): trustworthy deploy diff, dashboard coverage, guard fails closed#427
karolko9 wants to merge 14 commits into
masterfrom
ops/0455_observability-second-slice

Conversation

@karolko9

Copy link
Copy Markdown
Collaborator

Summary

  • Dashboard gains the two signals the alarms already page on — CH write failures and enrichment worker errors — plus a cost section, so a page has a picture to land in.
  • cdk diff stops lying. Reading a deployed template back mangles non-ASCII (byte-checked: the live alarm carries e2 80 94, the read returns 3f), so nine alarm descriptions and one widget title showed as changes that survive being deployed. Synthesized strings are ASCII-only now, with a header comment recording why — the diff is the only gate before a production deploy, and one that always shows ten false entries teaches the reader to scroll.
  • The declared-vs-emitted guard fails closed. Its filter-minted metric rule was derived from proximity, which accepted a fabricated metric in a test; it is now an explicit list, so forgetting an entry fails CI instead of passing green.
  • T4 resolved in the guard itself: it stays a grep and is deliberately not extended. The two known gaps (metric dimensions, filter-to-log-group pairing) are recorded as limits with the triggers that flip the decision. Measured before deciding — the filter half is exact, the metric-name half is fail-closed, and Nx does invalidate on Rust changes.
  • Four alarms stop re-deriving ADR 0054 from scratch; one refactor was built, measured (code −6, comments +8) and reverted rather than kept on faith.

Refs #422 predecessor. Task 0455.

…ricFilter

Second slice, first item. The processor fails in two disjoint modes and
each is invisible in the other's metric: a crash raises Lambda Errors
but a failed CH write does not (the handler reports batch-item failure
so SQS redelivers — Errors stayed 0 through the whole 0454 outage),
while ChWriteFailures, the filter-minted metric the zero-tolerance
alarm reads, counts exactly those quiet failures. The errors widget now
carries both series, so a ch-write-failures page has a dashboard answer
instead of a widget showing zero.

The declared-vs-emitted guard gains a second legitimate publisher kind:
a name read via namespace: passes if Rust publishes it OR a
logs.MetricFilter in the same stack mints it (the filter's PATTERN is
already guarded by the filter-literal suite, so the chain stays covered
end to end with no link checked twice). Without this the widget's read
of ChWriteFailures — which never occurs in Rust by construction —
failed CI. Sabotage-verified on this branch: a fabricated
BogusNeverPublished read goes red with the new message; restored, 5/5.
The relaxation shipped in 9d23043 inferred filter-minted metric names
by proximity — every metricName within 400 characters of a
metricNamespace:. Smell-tested on the operator's hunch and it failed:
a fabricated metric read inserted next to the filter block was accepted
silently (6/6 green). Proximity is not provenance, and the failure was
in the one direction a guard must never take.

Replaced with an explicit one-entry list naming the filter that mints
the name. Adding an entry is now a deliberate act; forgetting one fails
CI loudly, which is the safe direction. Same smell test now goes red
with a message that says what to do.

Noted in the comment: the exact form of this check asserts against the
synthesized template (Template.fromStack), where filter transformations
and dashboard bodies are real resources instead of source text — worth
building if the list ever grows past a couple of entries.
…y blank line

H1: the CH-write matrix cell said "partial" — the errors widget now
carries the ChWriteFailures series, so the cell names it.

H2: the escape hatch gains the cold-start query. The widget removed in
the first slice graphed InitDuration, which is not a CloudWatch metric
at all; Logs Insights does parse it off every REPORT line, so the data
was never lost — only the way to reach it was undocumented.

Also strips a trailing blank line that 9d23043 carried in: it was
left by that commit's sabotage test (append comment, delete comment,
blank line survives), and the pre-commit hook did not catch it because
it runs a single `nx format:write` pass and that command is not
idempotent — the same trap recorded in 6e37fb4. `format:check --all`
was red on the branch tip until now.
D6 — the last alarm without a widget (enrichment worker error rate)
gets one. Plots the raw error COUNT, not the alarm's errors/invocations
ratio: at this worker's traffic the ratio is unreadable (a 1-of-1
window is 100%), while the count is what an operator compares against
the enrichment DLQ depth sitting beside it — errors climbing with a
flat DLQ means the retries are absorbing them.

D8 — Cost section, the dashboard answer 0449 asked for. Cost Anomaly
Detection publishes no CloudWatch metric, so the only graphable spend
signal is AWS/Billing EstimatedCharges: cumulative month-to-date (the
slope is the daily burn, the line resets on the 1st), ~6 h refresh,
us-east-1 only, account-wide — the same scope the anomaly monitor
watches, so alert and graph cannot disagree. It exists for the case the
monitor structurally cannot see: slow creep never looks like a step
change, and budgets were dropped 2026-08-10.

Layout: the failure row was at five widgets (30 of 24 units), so
concurrency moves to its own context row. The failure row now reads as
'every one of these has an alarm behind it'.

Also records, in the guard's module doc, the three-way comparison
behind its shape (grep test / synthesized-template assertions / SSOT
codegen) with the trigger that should promote it — so the next reader
inherits the decision instead of re-arguing it.
…d slice

The branch predated the alarm-mute hotfix and still carried the
single-statement topic policy that caused it. Merging first means the
second slice builds on the fix rather than colliding with it.

One conflict, docs/runbooks/health.md coverage matrix, resolved as a
union: develop's canary row and delivery-chain incident record kept,
this branch's CH-write dashboard cell grafted over develop's
placeholder. No cell dropped from either side.
…as permanently dirty

cdk diff reads the deployed template back through a path that mangles
non-ASCII. Byte-checked 2026-08-19: the live alarm carries e2 80 94 (an em
dash) while the read returns 3f (?). Nine alarm descriptions and one dashboard
title therefore showed as changes that survive being deployed - ten entries
that never go away.

That matters because the diff is the only gate before a production deploy. A
gate that always shows ten false entries teaches the reader to scroll, and the
topic-policy change that muted every alarm for 19 hours on 2026-08-18 was in a
diff that was read and approved.

Em dash to hyphen, arrow to ->. Comments untouched; they never reach the
template. A header comment records why, so the typography is not restored as
an improvement later.
…ionale reach the slice

One conflict, the same coverage-matrix table as the previous merge, resolved
the same way: develop's side kept (the delivery-chain cell now reads DECIDED
rather than OPEN DECISION, and the note naming the config as the source of
every threshold quoted in the table), with this branch's CH-write dashboard
cell grafted over develop's 'partial' placeholder. Verified no cell was lost
from either side: 17 rows before and after.
ADR 0054 rule 4 — one knowing page per planned pause is cheaper than
suppression logic — was argued from scratch in three separate alarm comments,
and rule 3's paging-twice-mutes-alarms in a fourth. Each restatement was
correct and none was needed: the rule has a home, and a citation is shorter
than a re-derivation and cannot drift from it.

Cut: the backlog-age discriminator paragraph, the >10-threshold rejection
rationale, the 5xx paragraph restating rule 2 verbatim, and one clause in the
lag alarm pointing at coverage the alarm below already claims.

Kept, deliberately: every measured number, every incident that explains why an
alarm has the shape it has, the ASCII and topic-policy DANGER blocks, and the
tombstone recording two removed widgets. Those are the point of the file.

Net 11 lines. The review that raised this described four deletable essays; the
file does not contain them — it contains one argument repeated four times,
around measurement records that earn their space. Comment ratio 0.68 -> 0.66.

Build green, tests 4/4.
The processor and enrichment-worker error-rate alarms were 19 code lines
each and differed in five: construct id, alarm name, description, which
function feeds the expression, and the series label. The expression, both
periods, the comparison and evaluationPeriods were duplicated, so the shape
had to be changed twice or the two diverged unnoticed.

threshold and treatMissingData stay at the call site on purpose. The number
must not become a fourth place a threshold lives, and 'no datapoint' means
something different for each Lambda — 0 invocations is a pause or a dead
input for the processor, and the normal state for the gated worker. ADR 0054
rule 3 wants that reason beside the alarm; a shared default would erase it,
which is how an alarm slept through a 16 h stall on 2026-07-08.

Measured, because the number matters more than the intent: code 650 -> 644,
comments 430 -> 438. Six lines of code saved, eight spent explaining the
seam. Extraction at two call sites does not pay for itself in volume — what
it buys is that the shape exists once. Recorded here so the trade is visible
rather than assumed; the same accounting is why the whole-file alarm factory
(finding 18) was rejected.

Construct ids unchanged, so logical ids and the deployed resources are
untouched. Build green, tests 4/4. Template equality is not machine-checked
here — cdk diff before deploy is that gate, and it is now trustworthy.
Two gaps were raised: this test checks neither the metric dimensions an alarm
reads nor which log group a filter reads against which crate emits the
literal. Both are real. Neither is fixed, and that is the decision, not an
omission: closing them inside a source-text scraper means adding two more
heuristics, which the header already ruled to mean 'go to the synthesized
template instead'.

The template version buys exactness for one filter, one namespace, two metric
names and one log group, and costs a fixture for a constructor taking two
Lambdas, three queues, an ECS cluster and a REST API. On the same arithmetic
that rejected the alarm factory and the metric constants, it does not pay yet.

Three things were measured before deciding, and are recorded so nobody
re-derives them:
- the filter-literal half is EXACT — a renamed emit site fails, and so does a
  rename to a superstring of the declared literal, because the pair regex
  requires the closing quote;
- the metric-name half is the inexact one, and is already fail-closed through
  an explicit list;
- Nx does track the Rust sources this test reads — breaking an emit site
  invalidates the cache and the test re-runs, which matters because a cached
  pass here would be a silent skip of the guard against silent skips.

One trigger added: an emit site moving out of crates/indexer. That is the one
live scenario the log-group gap would hide.
…erge

The CH-write dashboard cell was grafted over develop's placeholder when the
branches were reconciled, which left that row's column padding off. Formatter
only; no cell content changed. Caught by running the CI format gate locally
rather than after a red run.
karolko9 added a commit that referenced this pull request Aug 20, 2026
Sixty-eight findings came out of the post-incident review and every one now
has a disposition; the task README had no record that any of it happened.
Added: where the 68 went, what shipped from them, and the lesson that cost
most of the effort.

Every finding naming a file and a line held up. Every aggregate
characterisation did not — 'about 600 lines of copy-paste' was 226 in total,
'~30 findings on other people's files' was nine files all belonging to this
operator, 'four justification essays' was one argument repeated four times.
Ten findings died on arithmetic that was never done when they were written.
That is why the alarm factory, the error-rate helper and the shared metric
constants were rejected or reverted instead of shipped on three reviewers
agreeing.

Three acceptance criteria corrected rather than ticked:
- dashboard/alarm coverage advanced with PR #427, and records that sharing a
  metric constant between alarm and widget was skipped despite favourable
  arithmetic, so their equality stays an assertion;
- latch-proofing now states that production contradicts it knowingly — the
  enrichment DLQ alarm has been in ALARM for 47 days and is mute by
  construction, skipped on the operator's call;
- the child-task criterion names the five tasks spawned (0507-0511) and the
  four extended, and records 0449 moving backlog to active because its
  detection half has been live since the last release.

Still ten criteria open. None was ticked today, because nothing was verified
today that would tick one.
karolko9 added a commit that referenced this pull request Aug 20, 2026
…d as written

Two close honestly. Cost: the monitor and its IMMEDIATE subscription to the
alarm topic were confirmed by read-only checks after the deploy, and the
boundary is written down rather than glossed — the last hop to the channel is
not proven, which is true of every alarm here and is what ADR 0054 rule 5 now
gates. API types: a stated N/A is answered, not pending.

The eight left are sorted into three shapes so the next session does not
re-derive them. Four need a production window — a deliberate pause, a
simulated stall, a real per-stack release tag. One is a measurement that the
deploy has now unblocked. One is in flight with PR #427. One is an operator
decision on re-scoping child tasks. One is gated on another.

The eighth cannot be ticked at all: 'no alarm can sit latched and mute' is
contradicted by an alarm that has been in ALARM for 47 days and was skipped
knowingly. Both honest ways out are recorded — drain the queue and let the
criterion mean what it says, or narrow it and write the reason. Leaving it
open and unexplained is the only option that makes the task lie.

Four of seven remaining need production, not code. That is the real state of
this umbrella: the building is done, the proving is not, and the proving is
deliberately not something that can be faked from here.
Base automatically changed from develop to master August 25, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant