feat(monitoring): settle the scrape path for smaug, the first scraped host - #508
Merged
Merged
Conversation
… host Every host in this estate pushes: Alloy remote_writes to 10.0.99.20 and Prometheus is never told the host exists. ADR-0016 reversed that for the NAS, because CasaBonita is terminal-outward and nothing on it may initiate upward — so Prometheus reaches in and scrapes instead, making smaug the estate's first scraped HOST rather than a container or a device behind an exporter. ADR-0040 then put TrueNAS on that host and opened a fork this issue had to settle: node_exporter on 9100, as the issue specified, or TrueNAS's own metrics endpoint on another port. THE ANSWER IS node_exporter, run as a digest-pinned container out of stacks/media. The 99 -> 40:9100 pass already exists and is verified in position; host-overview.json and seven rules in host.rules.yaml are built entirely on the node_* namespace; and a container here stays inside Dependabot, the digest pins and make validate, which is ADR-0040 decision 2's argument for this stack living in the repository at all. Said plainly in targets/node.yaml, because it matters: the alternative was declined on the pass that exists and the dashboard that exists, and NOT on a comparison. This repository records nothing about what TrueNAS natively exposes — four documents restate the same unsourced sentence. The target ships COMMENTED OUT. TrueNAS has no package manager and an immutable root, so every route to an exporter there runs through Docker and therefore through the apps pool, which waits on the drives. A live target would mean up == 0 and InstanceDown paging urgent every four hours forever — ADR-0017's refusal, and the call blackbox-dns.yaml and the lab's Windows job both already make. ADR-0040's "the scrape, then the stack" is right about authoring order and not achievable for liveness; build-the-nas.md 6.1 is the one-line uncomment that finishes it, with the check that catches a wrong --path.rootfs first. On the alerting question the issue asked: InstanceDown is `up == 0` with no job matcher, so it covers a scraped host for free and is already unit tested. The real gap was the other half — a target that stops being a target. An emptied or unparseable targets/node.yaml makes the series vanish rather than fall to 0, so up == 0 matches nothing, and RemoteWriteJobStale excludes directly scraped jobs by design. That hole was exactly the width of the one host Prometheus has to be told about. ScrapeTargetDisappeared closes it, mirroring RemoteWriteJobStale's shape and bound. Measured on the pinned image rather than assumed, as stacks/media asks: it declares user nobody, ships wget and no curl, has NO /-/healthy (404 — the four siblings healthchecked that way all serve it), boots read-only as 65534 with cap_drop ALL, and idles at 3.4 MiB. And the one that changed a decision: a bridged container reads its own netns, so node_network_* named the container's eth0 on a host with real NICs — which passes host-overview's device filter and would have been charted as this NAS's throughput. Those collectors are disabled; reporting nothing is honest, reporting a believable wrong number is not. The four test cases were mutation-tested. `for: 15m -> 0m` SURVIVED the first round, because a firing assertion at 60m cannot tell it from `for: 0m`; the eval_time: 40m pairing is what kills it. Recorded in the test file rather than quietly fixed. The job is called `node` and not `smaug-metrics`: that name would enrol a pulled job in RemoteWriteJobStale, whose notification text says an Alloy agent stopped pushing — false twice over about the one host with no Alloy agent by decision, and quietly so, because the InstanceDown inhibit would suppress the duplicate. The cost is one dashboard expression. Also folded in, because this change makes them wrong or found them wrong: security.md still said three CasaBonita passes and "neither is created yet" (four, created 2026-09-16); observability.md said Prometheus scrapes nine jobs (eleven before this, twelve after); dependabot.yml had no /stacks/media entry, so Jellyfin's pin was already rotting unnoticed. Refs #256 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gerrrt
force-pushed
the
gerrrt/smaug-scrape-path-d8637b
branch
from
September 18, 2026 02:51
b686378 to
36c4eb4
Compare
…ath-d8637b # Conflicts: # .github/dependabot.yml # docs/network.md # docs/runbooks/build-the-nas.md # stacks/media/README.md
…ath-d8637b # Conflicts: # README.md
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Settles the fork #256 inherited from ADR-0040 and builds the scrape path for
smaug— the estate's first host that is pulled rather than pushing.The fork, settled:
node_exporteron9100, as a digest-pinned container instacks/media. The argument lives inprometheus/targets/node.yaml's header, where the issue asked for it:host-overview.jsonis built entirely on thenode_*namespace — its only template variable islabel_values(node_uname_info, instance)— and seven rules inhost.rules.yamlcome free with it. The other branch means a parallel dashboard and parallel rules, for one host.10.0.99.20 → 10.0.40.30:9100pass already exists and is verified in position. The other branch is a fifth inbound rule with all of §0.5/§0.6's ordering care.make validate— ADR-0040 decision 2's argument for this stack being in the repo at all. ADR-0016 assumed a distribution package; TrueNAS has no package manager and an immutable root.Stated honestly in the file: the TrueNAS branch was declined on the pass that exists and the dashboard that exists, not on a comparison. This repository records nothing about what TrueNAS natively exposes — no port, no protocol, no metric names; four documents restate the same unsourced sentence. If someone measures it later and it is better, that is a new decision with new evidence.
The target ships commented out, and the issue's premise that it "doesn't wait on the ZFS pool" turns out to be wrong: every route to an exporter on TrueNAS runs through Docker and therefore through the apps pool. A live target means
up == 0andInstanceDownpagingurgentevery 4 hours forever — ADR-0017's refusal, and the callblackbox-dns.yamland the lab's Windows job already make.build-the-nas.md§6.1 is the one-line uncomment that finishes it.Why
On the alerting question the issue asked to check:
InstanceDownisup == 0with no job matcher, so a scraped host is covered for free — andhost.test.yamlalready tested it against a node_exporter-shaped series. The real gap was the other half: a target that stops being a target. An emptied or unparseabletargets/node.yamlmakes the series vanish rather than fall to 0, soup == 0matches nothing, andRemoteWriteJobStaleexcludes directly scraped jobs by design. That hole was exactly the width of the one host Prometheus has to be told about — the #62/#63 class.ScrapeTargetDisappearedcloses it.The job is
node, notsmaug-metrics. The convention would suggest the latter, but it would enrol a pulled job inRemoteWriteJobStale, whose notification says an Alloy agent stopped pushing — false twice over about the one host with no Alloy agent by decision, and quietly so, since theInstanceDowninhibit would suppress the duplicate. Cost of the honest name: one dashboard expression.Two measurements changed decisions rather than confirming them:
/-/healthyreturns 404 on node_exporter. Prometheus, Alertmanager, snmp-exporter and blackbox-exporter all serve it and are all healthchecked that way here. A healthcheck copied from a sibling file would have marked the container unhealthy forever onrestart: unless-stopped— a restart loop, for a path.node_network_receive_bytes_totalnamedeth0andloon a host with real NICs.eth0passes host-overview'sdevice!~"lo|veth.*|docker.*|br-.*"filter, so the container's quiet veth would have been charted as this NAS's throughput — on the one host whose job is moving bytes. Those collectors are disabled: reporting nothing is honest, reporting a believable wrong number is not. (This is likely already true of every Alloy host in the estate; filed separately, not fixed here.)Mutation testing caught a weak test.
for: 15m -> 0msurvived the first round — a firing assertion at 60m cannot tell it fromfor: 0m. Theeval_time: 40mpairing is what kills it. Recorded in the test file rather than quietly fixed.Also folded in, because this change makes them wrong or found them wrong:
observability.mdsaid Prometheus scrapes nine jobs (eleven before this, twelve after), anddependabot.ymlhad no/stacks/mediaentry, so Jellyfin's pin was already rotting unnoticed.Blast radius
9100pass already existed and is used, not created9100is published on a host that is not deployed; when it is, the segment could already reach it, which is recorded indocs/security.mdas a residualstacks/mediais undeployed and the scrape target is commented out, so the running stack gains anodejob with zero targetsprometheus.yamlis a single-file bind mount — applying this on the monitoring host needsmake up, which runscheck_mounted_config.py --fixVerification
make validatepasses (3 skips, all "not the deployment checkout")promtool test rules— 11 files, all SUCCESS, including 4 newScrapeTargetDisappearedcases and asmaugInstanceDowncaseunless→and,[24h]→[5m], drop thejob="node"selector,for: 15m→0mamtool config routes test— 8 assertions, unchanged;warning+availabilitywas already in usecheck_compose_health.py --probeexecswgetinside the pinned imagecheck_docs.pyclean for every count this PR touches (83 rules, 63 tested, 141 panels)Note
An earlier push of this branch showed a red
Validate configs. That was inherited frommain, which was failing at95b7621because two PRs each landed an ADR numbered 0041 and theREADME.mdADR/runbook counts went stale in the same merges. Fixed onmainby #503, and this branch has been rebased onto107256f, wherecheck_docs.pyis clean. Nothing in this PR was ever implicated.Uses
Refsrather thanCloses: all seven of the issue's checkboxes are done, but the scrape is not live until the pool exists and the four lines are uncommented.🤖 Generated with Claude Code