Skip to content

docs(runbook): replace the laptop cell in prometheus (#454) - #497

Merged
Gerrrt merged 4 commits into
mainfrom
gerrrt/prometheus-battery-monitoring-3b5e8f
Sep 18, 2026
Merged

Gerrrt merged 4 commits into
mainfrom
gerrrt/prometheus-battery-monitoring-3b5e8f

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Sep 17, 2026

Copy link
Copy Markdown
Owner

What changed

A runbook for swapping the A1437 cell in prometheus and proving the swap
worked — docs/runbooks/replace-the-laptop-cell.md. Three existing pointers
retargeted at it, and README's runbook count goes 25 to 26 in the same commit
because check_docs.py counts the glob.

Nothing about the running stack changes. This is documentation plus one
Alertmanager silence, described below.

Why

The repository half of #454
landed in #461 on 2026-09-12 and
the cell was bought on 2026-09-13. The procedure that actually settles that
issue — the fit, and the mains-pull test that proves the property the cell was
bought for — existed only as two sentences inside a UPS runbook and one bullet
in hardware.md, for a glued pouch cell in the machine the whole observability
stack runs on.

The issue stays open. It is settled by the fit and the test, not by this page,
so this is Refs and not a closing keyword.

No collector, and the reason is now in two places rather than one. Alloy's
node collector already exports node_power_supply_* from both laptops. The
issue body asked for energy_full / energy_full_design; neither exists on
either machine — both report the charge_* family — so a collector written to
that text would have emitted an empty file.

Two judgement calls worth reviewing:

  • The mains pull is deliberately not silenced, and runs twice.
    HostOnBattery is critical on the power route and will really page. That is
    the point: verify-the-alert-path.md proves the transport with Watchdog,
    which is vector(1) and carries no labels, so nothing else exercises this
    rule's for:, its join against node_power_supply_info{type="Mains"}, or its
    routing. Running it first on the old cell — which is also how a cell gets
    discharged before removal — means a failure after the swap is the cell or the
    adapter and nothing else. Both sibling battery runbooks record deleting a
    silence late as their single regret.
  • A new section neither sibling needed: what goes blind while the lid is
    off.
    shiva and mjolnir are not the monitoring host. prometheus is, so
    no alert can fire or be recorded during the window, and the only signal that
    survives it is the off-host healthcheck — which the runbook says to leave
    alone precisely because it will fire.

Blast radius

  • No change to network segmentation or firewall rules
  • No new port published to a VLAN that could not already reach the service
  • No credential added outside secrets/*.sops.yaml

No rules, tests, compose files or scripts are touched, so nothing deploys and
no alert behaviour changes from the diff itself.

One live change, made outside the diff: HostBatteryHealthLow had been
firing unsilenced for oracle since 2026-09-14 at 72 % of design, against a
cell that is unbought and second in line. Silenced
01cb81d7-5e19-4e6d-b386-f5c8c843032b until 2026-10-08, sharing an expiry with
the #351 disk silence on the
same host so one look covers both. Because the rule is < 0.8 and no label
carries the ratio, that silence hides any further decay of the cell as well as
the 72 % it was created for — recorded in the new runbook's status banner and
its open items rather than only in Alertmanager.

Verification

  • make validate passes — see the note below
  • Deployed to the lab and confirmed working — n/a, documentation
  • Docs updated

python3 scripts/check_docs.py prints docs OK — 82 Prometheus + 18 Loki rules, 7 dashboards, 141 panels, 10 assertions and exits 0. Reverting the
README count to 25 makes it print README.md:96 claims 25 runbooks; the repository has 26 and exit 1, so the bump is load-bearing rather than
cosmetic. ./scripts/lint.sh passes all five linters and markdownlint-cli2
reports 0 issues on the new file. Re-run after rebasing onto fc4490e.

The local ./scripts/validate.sh run was abandoned rather than completed.
Thirteen other worktree sessions were running it concurrently against the
shared Docker daemon at load 36, and it had not finished after twenty minutes.
Since this commit touches no rule, test or config, its promtool stages cannot be
affected by the diff, and CI on a clean runner is the better authority — so the
box above is ticked on CI rather than on that run. Flagging it rather than
implying a green local run.

Every metric name in the runbook was read back from the host's own Prometheus
on 2026-09-17 and all twelve return a count. Three baseline figures were
corrected before publishing: charge_ampere, voltage_volt and
temp_celsius are live-varying, and an instantaneous sample was being recorded
as though it were a fixed fact. temp_celsius in particular, which the safety
section leans on, ranges 32.7–39.2 over 24 hours averaging 33.3, so it is now a
band with a note that a single reading near 39 is ordinary. charge_full is
confirmed static at 6.196 across the retained window, so it is a real baseline.

Left for a separate issue

HostBatteryHealthLow's description names
docs/runbooks/fit-the-ups-battery.md, so the alert for a laptop cell points
at the UPS runbook. Correcting it means editing the rule and the
exp_annotations block that asserts the string verbatim — a rules-and-tests
change, not a docs one. Recorded under What is still open.

The dashboards carry no battery panel at all: power_supply appears in no
Grafana JSON, while every other hardware signal has one.

🤖 Generated with Claude Code

Gerrrt and others added 2 commits September 17, 2026 23:14
The repository half of #454 landed in #461 on 2026-09-12 and the cell was
bought on 2026-09-13. What was never written down is the procedure that
actually settles the issue: the fit itself, and the test that proves the
property the cell was bought for. It existed as two sentences inside a UPS
runbook and one bullet in hardware.md, for a glued pouch cell in the machine
the whole observability stack runs on.

This gives it its own page, modelled on replace-the-smart-storage-battery.md
(baseline, fit, confirm the metrics moved, what is still open) with one section
neither sibling needed: what goes blind while the lid is off. shiva and
mjolnir are not the monitoring host. prometheus is, so the window has to be
bounded, backed up, and measured afterwards rather than assumed — and the only
signal that survives it is the off-host healthcheck, which is told to be left
alone precisely because it will fire.

No collector, and the reason is now in two places rather than one. Alloy's
node collector already exports node_power_supply_* from both laptops. The
issue body asked for energy_full and energy_full_design; neither exists on
either machine, both report the charge_* family, and a collector written to
that text would have emitted an empty file. The runbook names all three
spellings that do not survive a guess — cyclecount not cycle_count, charge_*
not energy_*, charge_ampere for what would be charge_now — because a wrong
metric name returns an empty result, which on a command line is
indistinguishable from the cell being gone.

The mains pull is deliberately not silenced, twice. HostOnBattery is critical
on the power route and will really page, and that is the point:
verify-the-alert-path.md proves the transport with Watchdog, which is vector(1)
and carries no labels, so nothing else in the estate exercises this rule's
for:, its join against node_power_supply_info{type="Mains"}, or its routing.
Running it once on the old cell before removal — which is also how a cell gets
discharged before it comes out — means a failure after the swap is the cell or
the adapter and nothing else. Both sibling runbooks record deleting a silence
late as their single regret; a silence never created cannot be forgotten.

Three existing pointers retargeted at the new page: the laptop-cell paragraph
in fit-the-ups-battery.md, the A1437 accessory entry in hardware.md, and the
host.rules.yaml row in observability.md. README's runbook count goes 25 to 26
in the same commit, because check_docs.py counts the glob and CI fails
otherwise.

Deliberately not touched. host.rules.yaml and host.test.yaml: HostBatteryHealthLow's
description names fit-the-ups-battery.md, so the alert for a laptop cell points
at the UPS runbook — correcting it means editing the rule and the
exp_annotations block that asserts the string verbatim, which is a
rules-and-tests change rather than a docs one, and it is recorded under What is
still open. roadmap.md: a record, excluded from check_docs.py, and its entry
moves when the cell is fitted rather than when this page lands. The dashboards
carry no battery panel at all, which is a real gap and its own issue.

Three baseline figures were corrected against the host before publishing.
charge_ampere, voltage_volt and temp_celsius are live-varying, and an
instantaneous sample was being written as though it were a fixed fact —
temp_celsius in particular, which the safety section leans on. It ranges 32.7
to 39.2 over 24 hours averaging 33.3, so it is recorded as a band and the page
says a single reading near 39 is ordinary. charge_full is confirmed static at
6.196 across the retained window, so it is a real baseline.

Separately, HostBatteryHealthLow had been firing unsilenced for oracle since
2026-09-14 at 72 % of design, against a cell that is unbought and second in
line. Silenced 01cb81d7-5e19-4e6d-b386-f5c8c843032b until 2026-10-08, sharing
an expiry with the #351 disk silence on the same host so one look covers both.
The silence and what it also hides — the rule is < 0.8 and no label carries the
ratio, so further decay is invisible too — are recorded in the new runbook's
status banner and its open items, rather than only in Alertmanager.

Verified: python3 scripts/check_docs.py prints "docs OK — 82 Prometheus + 18
Loki rules, 7 dashboards, 141 panels, 10 assertions" and exits 0; reverting the
README count to 25 makes the same command print "README.md:96 claims 25
runbooks; the repository has 26" and exit 1, so the bump is load-bearing rather
than cosmetic. ./scripts/lint.sh passes all five linters, and markdownlint-cli2
reports 0 issues on the new file. Every metric name in the runbook was read
back from this host's Prometheus on 2026-09-17 and all twelve return a count:
charge_full 6.196, charge_full_design 6.6, charge_ampere 6.122, capacity 93,
cyclecount 108, present 1, voltage_volt 12.436, voltage_min_design 11.21,
temp_celsius 39.1 at the sample and 32.7-39.2 over 24h, current_ampere 0,
online{power_supply="ADP1"} 1, and an info series carrying manufacturer SMP,
model_name bq20z451, technology Li-ion, status Full. The silence was read back
active with the alert reporting state suppressed.

Refs #454

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six pull requests landed on main while this branch was open. The only conflict
was README.md's counted-claims bullet: main bumped the ADR count for ADR-0041
while this branch bumped the runbook count for replace-the-laptop-cell.md, and
both edits are wanted.

Resolved by keeping both intents and then reading the real numbers off the
repository rather than trusting either side's arithmetic — the merge brings in
two runbooks besides this branch's, so the true figures are 42 ADRs and 28
runbooks, not the 41 and 26 the two sides each believed.

Not fixed here, because it is main's and not this branch's: two ADRs both claim
the number 0041, so check_docs.py fails on main by itself, at fc4490e and at
84418ae, with this branch absent. Renumbering one of them is a decision about
another change's content and does not belong in a runbook PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in #503, which renumbered the duplicate ADR-0041 to 0042 and was the
only thing failing this branch's Validate job. main now carries 0041 for the
CRS326 and 0042 for the remote path, and check_docs.py passes on it again.

The conflict was README.md's counted-claims bullet once more, and for the same
reason as the last merge: main had corrected the runbook count to 27 for its
own additions while this branch held 28 for those plus this one. Resolved to 28
and then read off the repository rather than reasoned about — 42 ADR files and
28 runbook files, which is what check_docs.py now agrees with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gerrrt added a commit that referenced this pull request Sep 18, 2026
HostBatteryHealthLow reads a laptop cell's measured full capacity against
its design figure, and fires today for oracle's BAT0 at 72 %. Its
description sent the reader to docs/runbooks/fit-the-ups-battery.md,
which is the rack pack in mjolnir: an APCRBC115 in a Smart-UPS, a
self-test to watch and an NMC to read. None of that is what you do with a
thirteen-year-old cell in a laptop on a shelf, so the one alert whose
whole action is "order the replacement" pointed at the wrong hardware.

It now names docs/runbooks/replace-the-laptop-cell.md, which carries the
baseline to take first, the fit, the mains-pull test that proves it, the
stack-down window and the disposal. Because that runbook covers the
before as well as the after, the sentence reads "what to check before and
after the swap" rather than only after.

That runbook lands with #497, which is still open — this change should
not merge ahead of it, or the annotation names a path that does not yet
exist on main.

The description is asserted verbatim in two exp_annotations blocks in
tests/host.test.yaml, so the rule and both assertions move together.
Line-wrapping differs between the two files and always has; the folded
scalars were compared as strings to confirm they still produce identical
text.

HostOnBattery and HostBatteryNotReported were checked and left alone:
neither names a runbook, so neither had a wrong one. Each already carries
its own next step in prose — UpsOnBattery and node_power_supply_capacity
for the first, /sys/class/power_supply for the second. The
fit-the-ups-battery.md reference in the block's header comment also
stays: it cites that document for stating the shelf's mains-cut design,
which is still true, not for how to change a cell.

Verified: `promtool test rules` on tests/host.test.yaml via docker with
the pinned prom/prometheus:v3.14.0 image (the Makefile's check-rules
target calls a bare `promtool`, which is not on PATH on this host) —
SUCCESS. Reverting one of the two assertions to the old path was
confirmed to FAIL the run, proving the annotation is genuinely asserted
and not silently ignored. `./scripts/validate.sh` exits 0, all checks
passed, including "observability: promtool test rules (11 file(s))";
the 3 skips are the systemd unit checks that only run in the deployment
checkout.

Refs #454

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cell was delivered a few hours before this commit. Two documents said "in
transit" and now do not.

The baseline was re-read on the day it arrived rather than assumed to have
held: charge_full 6.196 Ah of 6.6 Ah, 108 cycles, capacity 93 — identical to
the 2026-09-17 figures the runbook's table records. So the table is current as
it stands and does not need retaking before the fit, which the status banner
now says outright, because the one thing that would quietly invalidate the
whole comparison is a baseline taken at a different time from the one written
down.

hardware.md's entry also carried a condition that has now come due. It records
the cell as compatible rather than genuine "until it is in hand", because the
listing called it genuine while its brand field said unbranded. It is in hand,
so that sentence can now be settled by looking at the pack instead of at the
listing — the line is left as it is, deliberately, and says it stays that way
until someone reads the markings on the cell itself. Recording it as an Apple
part on the strength of a listing that contradicts itself would be the same
error as trusting a UPS card's fabricated pack.

Nothing is fitted and nothing is measured differently. What settles #454 is
still the fit and the mains pull afterwards, so this is Refs and the issue
stays open.

Verified: python3 scripts/check_docs.py prints "docs OK — 82 Prometheus + 18
Loki rules, 7 dashboards, 141 panels, 10 assertions" and exits 0;
./scripts/lint.sh passes all five linters.

Refs #454

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Gerrrt
Gerrrt force-pushed the gerrrt/prometheus-battery-monitoring-3b5e8f branch from 9c161bd to a009709 Compare September 18, 2026 02:42
@Gerrrt
Gerrrt merged commit b5d67e6 into main Sep 18, 2026
3 checks passed
@Gerrrt
Gerrrt deleted the gerrrt/prometheus-battery-monitoring-3b5e8f branch September 18, 2026 02:59
Gerrrt added a commit that referenced this pull request Sep 18, 2026
Five more PRs landed, two of them runbooks (#497, #507), and main's
README count had not moved with them. The merged tree has 43 ADRs and
30 runbooks; README now says so, which check_docs.py requires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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