diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 303ddadb..671c2d8e 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -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}"; }; diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index 06e9ec9d..4c8cc955 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -243,12 +243,26 @@ in ` = ":"`. **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