deploy: name the swarm collector swarm-otel, not otel

Per review: `deploy.otel` does not imply swarm level, and there is a
hive-tier collector too -- `services.hyperhive.otel.enable`, which every
hive runs. The tier is the entire distinction between the two, so the
name has to carry it, matching `deploy.swarm-controller` and
`deploy.swarm-ui`.

15 swarm-tier references renamed across 8 files. The hive-tier
collector's 16 references are deliberately untouched, verified as a
control on the same command.

Three spellings needed three different patterns, all inside this one
rename: the dotted path; the `mkRenamedOptionModule` target written as a
nix LIST (`[ ... "deploy" "otel" "enable" ]`), which no dotted grep can
match; and prose in docs/observability.md spelled `deploy.otel` with
neither `.enable` nor a leading dot. Unanchored `deploy\.otel\b` is the
only pattern that finds all three.
This commit is contained in:
atlas 2026-08-30 04:14:20 +02:00 committed by mara
commit 09fe94a8ca
10 changed files with 24 additions and 23 deletions

View file

@ -80,7 +80,7 @@ the host is:
| | runs where | receives from | does | | | runs where | receives from | does |
|---|---|---|---| |---|---|---|---|
| **hive tier**`otel.enable` | every hive with agents | that hive's agents, on the bridge | forwards to the swarm tier. Holds no credential, picks no destination | | **hive tier**`otel.enable` | every hive with agents | that hive's agents, on the bridge | forwards to the swarm tier. Holds no credential, picks no destination |
| **swarm tier**`deploy.otel` | once per swarm | every hive's collector | writes the swarm's store and exports upstream | | **swarm tier**`deploy.swarm-otel` | once per swarm | every hive's collector | writes the swarm's store and exports upstream |
An all-local host runs both, and needs nothing said about the hop between them. An all-local host runs both, and needs nothing said about the hop between them.

View file

@ -67,7 +67,7 @@ in
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "otel" "enable" ] [ "services" "hyperhive" "swarm" "otel" "enable" ]
[ "services" "hyperhive" "deploy" "otel" "enable" ] [ "services" "hyperhive" "deploy" "swarm-otel" "enable" ]
) )
]; ];
@ -131,7 +131,7 @@ in
''; '';
}; };
otel.enable = lib.mkOption { swarm-otel.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -142,11 +142,12 @@ in
metrics pair it feeds: a swarm has one of these, and it belongs metrics pair it feeds: a swarm has one of these, and it belongs
wherever the shared services live rather than on every hive. wherever the shared services live rather than on every hive.
Not to be confused with Named `swarm-otel` rather than `otel` because there are two
{option}`services.hyperhive.otel.enable`, the **hive-tier** collectors and the tier is the whole distinction:
collector, which every hive runs and which is a different option. {option}`services.hyperhive.otel.enable` is the **hive-tier** one,
A hive that does not run the swarm collector still runs its own, which every hive runs. A bare `deploy.otel` would not say which
and reaches this one by name at it meant. A hive that does not run the swarm collector still runs
its own, and reaches this one by name at
{option}`services.hyperhive.swarm.otel.domain`. {option}`services.hyperhive.swarm.otel.domain`.
''; '';
}; };

View file

@ -61,7 +61,7 @@ in
their own containers can reach. their own containers can reach.
That collector forwards to the swarm's That collector forwards to the swarm's
({option}`services.hyperhive.deploy.otel.enable`), which holds the ({option}`services.hyperhive.deploy.swarm-otel.enable`), which holds the
upstream credential and writes the swarm's store. So an agent never upstream credential and writes the swarm's store. So an agent never
sees the credential, and neither does this tier. sees the credential, and neither does this tier.
@ -81,7 +81,7 @@ in
has left the swarm. has left the swarm.
Read by the swarm's collector Read by the swarm's collector
({option}`services.hyperhive.deploy.otel.enable`), which is the only ({option}`services.hyperhive.deploy.swarm-otel.enable`), which is the only
tier that holds the upstream credential. An agent is handed the tier that holds the upstream credential. An agent is handed the
*first* hop instead this hive's own collector so this value is *first* hop instead this hive's own collector so this value is
never given to a container. never given to a container.

View file

@ -1010,7 +1010,7 @@ in
# the option it gates are visibly the same path: gating the wrong one is # 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 a build error, it is a target that is always declared or never is.
services.hyperhive.swarm.otel.scrapeTargets = services.hyperhive.swarm.otel.scrapeTargets =
lib.mkIf config.services.hyperhive.deploy.otel.enable lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
{ {
authelia = "127.0.0.1:${toString cfg.metricsPort}"; authelia = "127.0.0.1:${toString cfg.metricsPort}";
}; };

View file

@ -174,14 +174,14 @@ let
# so the two ends agree on exactly one path, `/v1/metrics`, arrived at # so the two ends agree on exactly one path, `/v1/metrics`, arrived at
# from opposite directions. # from opposite directions.
# #
# Gated on `deployCfg.otel.enable` alone (a swarm-wide fact, not "enabled # Gated on `deployCfg.swarm-otel.enable` alone (a swarm-wide fact, not "enabled
# HERE"): the collector is reachable by name wherever it runs, so a # HERE"): the collector is reachable by name wherever it runs, so a
# controller not co-located with it now exports over https instead of # controller not co-located with it now exports over https instead of
# exporting nothing — the graceful-absence case left is "no swarm-otel # exporting nothing — the graceful-absence case left is "no swarm-otel
# anywhere in this swarm at all," which `forgeEnv` already models the # anywhere in this swarm at all," which `forgeEnv` already models the
# same way. # same way.
otelSwarmCfg = config.services.hyperhive.swarm.otel; otelSwarmCfg = config.services.hyperhive.swarm.otel;
otelEnv = lib.optionalAttrs deployCfg.otel.enable { otelEnv = lib.optionalAttrs deployCfg.swarm-otel.enable {
OTEL_EXPORTER_OTLP_ENDPOINT = "https://${otelSwarmCfg.domain}/${otelSwarmCfg.producerName}"; OTEL_EXPORTER_OTLP_ENDPOINT = "https://${otelSwarmCfg.domain}/${otelSwarmCfg.producerName}";
# Checked by `swarm-otel.nix`'s `oidc/${producerName}` authenticator # Checked by `swarm-otel.nix`'s `oidc/${producerName}` authenticator
# against exactly this controller's own registered audience (see the # against exactly this controller's own registered audience (see the

View file

@ -409,7 +409,7 @@ in
# Declared here rather than in the collector's module, per that option's # Declared here rather than in the collector's module, per that option's
# rule: an entry exists where the service that named it runs. # rule: an entry exists where the service that named it runs.
services.hyperhive.swarm.otel.scrapeTargets = services.hyperhive.swarm.otel.scrapeTargets =
lib.mkIf config.services.hyperhive.deploy.otel.enable lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
{ {
grafana = "127.0.0.1:${toString cfg.metricsPort}"; grafana = "127.0.0.1:${toString cfg.metricsPort}";
}; };

View file

@ -536,7 +536,7 @@ in
# and only this one reads `scrapeTargets`. Written in full so the gate # and only this one reads `scrapeTargets`. Written in full so the gate
# and the option it gates are visibly the same path. # and the option it gates are visibly the same path.
services.hyperhive.swarm.otel.scrapeTargets = services.hyperhive.swarm.otel.scrapeTargets =
lib.mkIf config.services.hyperhive.deploy.otel.enable lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
{ {
nats = "127.0.0.1:${toString cfg.metricsPort}"; nats = "127.0.0.1:${toString cfg.metricsPort}";
}; };

View file

@ -180,7 +180,7 @@ let
collectLogs = logExporterNames != [ ]; collectLogs = logExporterNames != [ ];
in in
{ {
# `enable` moved to `services.hyperhive.deploy.otel.enable` — see ./deploy.nix. # `enable` moved to `services.hyperhive.deploy.swarm-otel.enable` — see ./deploy.nix.
# ⚠️ That is the SWARM collector. The per-hive one keeps its own # ⚠️ That is the SWARM collector. The per-hive one keeps its own
# `services.hyperhive.otel.enable` (./otel.nix) and is a different # `services.hyperhive.otel.enable` (./otel.nix) and is a different
# option entirely — every hive runs that one. # option entirely — every hive runs that one.
@ -435,8 +435,8 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.otel.enable) { config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-otel.enable) {
# The gateway name, inside `deployCfg.otel.enable` — that guard is the load-bearing # The gateway name, inside `deployCfg.swarm-otel.enable` — that guard is the load-bearing
# part. Every hive in a swarm may know this collector exists, but only # part. Every hive in a swarm may know this collector exists, but only
# the host that RUNS it may claim the name; a client hive declaring the # the host that RUNS it may claim the name; a client hive declaring the
# vhost would answer for a service it does not have. # vhost would answer for a service it does not have.
@ -601,7 +601,7 @@ in
# samples and drops them — which looks healthy and loses data. # samples and drops them — which looks healthy and loses data.
assertion = otelCfg.endpoint != "" || deployCfg.victoriametrics.enable; assertion = otelCfg.endpoint != "" || deployCfg.victoriametrics.enable;
message = '' message = ''
services.hyperhive.deploy.otel.enable is true but this collector services.hyperhive.deploy.swarm-otel.enable is true but this collector
has nowhere to send what it receives: has nowhere to send what it receives:
services.hyperhive.otel.endpoint is empty and services.hyperhive.otel.endpoint is empty and
services.hyperhive.deploy.victoriametrics.enable is false. services.hyperhive.deploy.victoriametrics.enable is false.
@ -633,7 +633,7 @@ in
# collector would listen on nothing while looking configured. # collector would listen on nothing while looking configured.
assertion = hyperhiveCfg.swarm.hives != { }; assertion = hyperhiveCfg.swarm.hives != { };
message = '' message = ''
services.hyperhive.deploy.otel.enable is true but services.hyperhive.deploy.swarm-otel.enable is true but
services.hyperhive.swarm.hives is empty: ingest is authenticated services.hyperhive.swarm.hives is empty: ingest is authenticated
per hive, so an empty roster means this collector accepts nothing per hive, so an empty roster means this collector accepts nothing
from anyone. from anyone.
@ -745,7 +745,7 @@ in
# issuer URL evaluates cleanly and refuses every hive at runtime. # issuer URL evaluates cleanly and refuses every hive at runtime.
assertion = autheliaCfg.url != null; assertion = autheliaCfg.url != null;
message = '' message = ''
services.hyperhive.deploy.otel.enable is true but services.hyperhive.deploy.swarm-otel.enable is true but
services.hyperhive.swarm.authelia.url is null: every hive services.hyperhive.swarm.authelia.url is null: every hive
authenticates to this collector as itself, and the token comes authenticates to this collector as itself, and the token comes
from the swarm's identity provider. from the swarm's identity provider.

View file

@ -79,7 +79,7 @@ in
# The swarm collector that feeds the metrics pair, and the only tier # The swarm collector that feeds the metrics pair, and the only tier
# holding the upstream credential. ⚠️ NOT the per-hive collector below, # holding the upstream credential. ⚠️ NOT the per-hive collector below,
# which every hive runs. # which every hive runs.
config.services.hyperhive.deploy.otel.enable = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.swarm-otel.enable = lib.mkDefault swarmCfg.enableRequiredServices;
# The metrics pair, deriving together on purpose: a store with no UI is # The metrics pair, deriving together on purpose: a store with no UI is
# unreadable and a UI with no store is empty, so there is no sensible # unreadable and a UI with no store is empty, so there is no sensible

View file

@ -60,7 +60,7 @@ let
# failed every POST and dropped the samples. # failed every POST and dropped the samples.
++ lib.optional deployCfg.grafana.enable swarmCfg.grafana.domain ++ lib.optional deployCfg.grafana.enable swarmCfg.grafana.domain
++ lib.optional deployCfg.victoriametrics.enable swarmCfg.victoriametrics.domain ++ lib.optional deployCfg.victoriametrics.enable swarmCfg.victoriametrics.domain
++ lib.optional deployCfg.otel.enable swarmCfg.otel.domain ++ lib.optional deployCfg.swarm-otel.enable swarmCfg.otel.domain
# VictoriaLogs' vhost is new (was previously unpublished entirely — see # VictoriaLogs' vhost is new (was previously unpublished entirely — see
# swarm-victorialogs.nix's file-top comment) and needs the same # swarm-victorialogs.nix's file-top comment) and needs the same
# membership every gateway-published swarm service needs: absent from # membership every gateway-published swarm service needs: absent from