Compare commits

..
2 changed files with 10 additions and 114 deletions

View file

@ -239,45 +239,6 @@ in
'';
};
monitorPort = lib.mkOption {
type = lib.types.port;
default = 8222;
description = ''
Port NATS serves its **monitoring** endpoint on, bound to
loopback.
Not a metrics endpoint: the server has no Prometheus format of its
own. This serves `/varz`, `/connz`, `/routez` as JSON, and the
exporter below is what translates it which is why enabling the
exporter without this produces a process that starts cleanly and
scrapes nothing.
Loopback, and the exporter is the only intended reader. The
endpoint is unauthenticated and `/connz` names every connected
client, so the address it binds is the whole access control. Do
not widen it, and do not put it behind a gateway vhost expecting
that to add one.
'';
};
metricsPort = lib.mkOption {
type = lib.types.port;
default = 7777;
description = ''
Port the Prometheus exporter serves NATS's metrics on, bound to
loopback for the swarm collector to scrape.
This and {option}`monitorPort` are two more claims on a port
space every swarm container shares they run in this container but
`privateNetwork = false`, so a collision with any other hyperhive
service is a runtime coin toss over which process gets the port,
with nothing in any log saying so. Both defaults are upstream's
own (`nats-server` 8222, `prometheus-nats-exporter` 7777) and
neither is claimed elsewhere in this repo, checked when they were
added.
'';
};
clientId = lib.mkOption {
type = lib.types.str;
default = "swarm-nats";
@ -482,31 +443,6 @@ in
}
];
# 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.
#
# ⚠️ 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.
#
# 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}";
};
containers.swarm-nats = {
autoStart = true;
ephemeral = false;
@ -573,36 +509,10 @@ in
# includes other files. The check moves to server start.
validateConfig = !cfg.autoGenerateCallout;
settings =
calloutBlocks {
userKey = cfg.calloutUserPublicKey;
issuerKey = cfg.calloutIssuerPublicKey;
}
// {
# The monitoring endpoint, which is what the exporter below
# reads. `//` adds a key `calloutBlocks` does not produce
# (`accounts`, `authorization`) — checked, because a shallow
# merge that collided here would drop the auth config while
# rendering a config the server starts on.
#
# ⚠️ Loopback: unauthenticated, and `/connz` lists every
# connected client.
http = "127.0.0.1:${toString cfg.monitorPort}";
};
};
# The translation layer. NATS has no Prometheus format of its
# own, so this reads the JSON monitoring endpoint above and
# re-serves it in the format the collector scrapes.
#
# Upstream's exporter module rather than a hand-rolled unit, for
# the same reason `services.nats`'s own `settings` is kept: the
# reviewed reasoning about flags and hardening lives there.
services.prometheus.exporters.nats = {
enable = true;
listenAddress = "127.0.0.1";
port = cfg.metricsPort;
url = "http://127.0.0.1:${toString cfg.monitorPort}";
settings = calloutBlocks {
userKey = cfg.calloutUserPublicKey;
issuerKey = cfg.calloutIssuerPublicKey;
};
};
# A wrapper that includes upstream's rendered settings verbatim

View file

@ -243,26 +243,12 @@ in
`<job name> = "<host>:<port>"`.
**A service declares its own entry, from its own module, under its
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.
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.
Samples land in a swarm-level pipeline that stamps `swarm` and
**never** `hive`: a swarm service does not belong to a hive, and