docs(swarm-otel): scrapeTargets constrains the target, not the scraper

The option's description claimed declaring an entry from the service's
own module put 'the scraper and the target on the same host by
construction rather than by luck'. It does not. It constrains where the
target is; nothing in it places the collector, and the two enable flags
are co-located by a shared lib.mkDefault rather than by construction.

Split across hosts, a target is silently never scraped — the service's
host declares an entry no local collector reads, the collector's host
never enabled the service. No error surfaces, and no assertion can
catch it: separate hosts are separate evaluations with no shared
context, so the doc telling the truth is the only mechanism there is.

The same paragraph already warned co-location was not a guarantee, four
lines below the sentence claiming it was; a reader arriving for
permission stopped at the permission. This one did.

swarm-nats carries the concrete caveat for its own contribution.
This commit is contained in:
atlas 2026-08-24 14:48:38 +02:00 committed by mara
commit d2de3e8a2a
2 changed files with 39 additions and 20 deletions

View file

@ -482,22 +482,27 @@ in
}
];
# Declared here rather than in the collector's module, which is the
# rule the option carries: an entry exists only where the service that
# named it runs, so the scraper and the target are on one host by
# construction rather than by luck.
# Declared here rather than in the collector's module: an entry then
# exists only where the service that named it runs. Gated on a
# collector, because a target nobody reads asserts a collection that is
# not happening.
#
# Gated on the collector, because a target nobody reads is a config
# asserting a collection that is not happening. This does NOT make the
# queue scrapeable from another host — that would need the endpoint
# published under a name with a cert and an audience, and nothing here
# should be: both listeners above are deliberately loopback.
# ⚠️ KNOWN LIMITATION, and it is SILENT. That rule constrains the
# target, not the scraper — nothing places the collector on this host.
# `swarm-required-services.nix` derives `nats.enable` and `otel.enable`
# from one `lib.mkDefault`, so they are co-located by *default*, and an
# operator may split them. Split, NATS is never scraped: this host
# declares an entry no local collector reads, the collector's host never
# enabled this module. No error, no warning — a healthy exporter and an
# empty dashboard.
#
# ⚠️ `swarm.otel`, not `hyperhive.otel` — two collectors one word
# apart, and only this one reads `scrapeTargets`. Written in full so
# the gate and the option it gates are visibly the same path; gating
# the wrong one is not a build error, it is a target that is always
# declared or never is.
# Not guardable: separate hosts are separate evaluations with no shared
# context, so this one cannot see what that one runs. Saying so is the
# only mechanism there is.
#
# ⚠️ `swarm.otel`, not `hyperhive.otel` — two collectors one word apart,
# and only this one reads `scrapeTargets`. Written in full so the gate
# and the option it gates are visibly the same path.
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
nats = "127.0.0.1:${toString cfg.metricsPort}";
};

View file

@ -243,12 +243,26 @@ in
`<job name> = "<host>:<port>"`.
**A service declares its own entry, from its own module, under its
own `enable`.** That is what puts the scraper and the target on the
same host by construction rather than by luck: an entry exists only
where the service that named it runs. Do not assemble the list here.
Every swarm service being co-located is a property of the all-local
deployment, not a guarantee and that is precisely the case where
the difference is invisible until a swarm splits across hosts.
own `enable`.** Do not assemble the list here: an entry then exists
only where the service that named it runs, so a target is never
declared on a host that does not serve it.
**That constrains the TARGET, not the SCRAPER, and the difference
is a silent gap.** Nothing here puts the collector on the same host.
Services are co-located by *default* `swarm-required-services.nix`
derives their `enable` flags from one `lib.mkDefault` not by
construction, and an operator may split them.
When they are split the target is simply never scraped: the
service's host declares an entry no local collector reads, and the
collector's host never enabled that service so has no entry at all.
No error, no eval failure, no warning.
**No assertion can catch this.** Two hosts are separate NixOS
evaluations with no shared context, so neither can see what the
other runs. A service that would be seriously wrong to lose should
say so in its own contribution, because saying it is the only
mechanism available.
Samples land in a swarm-level pipeline that stamps `swarm` and
**never** `hive`: a swarm service does not belong to a hive, and