From 4de4878e7434338dff4cff999be4965bc9f0523c Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 24 Aug 2026 13:20:31 +0200 Subject: [PATCH] fix(swarm-otel): reserve the swarm tier's component names from hive names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collector names components `/` — a hive name for the per-hive pipelines, the literal `swarm` for the swarm tier's own. Both land in one attrset via `//`, so a hive named `swarm` replaced the swarm tier's parts and lost its own: its receiver kept accepting pushes into a pipeline that routed nowhere, and its samples lost the `hive` stamp that makes attribution unforgeable. Zero failed assertions. The reserved name is now bound once and interpolated at each swarm-tier use, so the guard checks the same string the config emits rather than a copy of it. A second swarm-tier pipeline joins the list and inherits the check without touching the assertion. --- nix/host-modules/swarm-otel.nix | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index b6a2156d..cb67b155 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -43,6 +43,21 @@ let autheliaCfg = hyperhiveCfg.swarm.authelia; + # The collector names its components `/`, where `` is a + # hive name for the per-hive pipelines and this literal for the swarm tier's + # own. The two share one namespace and are merged with `//`, so a hive named + # this would silently REPLACE the swarm tier's parts — and lose its own + # pipeline in the process, while its receiver keeps accepting pushes. + # + # Bound once and interpolated at every swarm-tier use below, so the assertion + # that reserves it is checking the same string the config emits. A literal + # repeated at each site would let the guard and the config drift apart, which + # is the failure this guard exists to prevent. + swarmTierName = "swarm"; + # Every `` this module claims for itself. One entry today; a second + # swarm-tier pipeline would be added here and inherit the check for free. + reservedOwners = [ swarmTierName ]; + # `attrNames` is sorted, so this is a function of the hive SET and not of # the order anyone wrote it in. # @@ -286,6 +301,33 @@ in List the swarm's hives. ''; } + { + # A hive whose name is one this module claims for itself collides in + # the collector's component namespace, and `//` resolves it silently: + # the swarm tier's parts win, that hive's `metrics/` pipeline + # disappears, and its `resource/` stamp goes with it — so the + # hive still connects and pushes into nothing, unlabelled. + # + # Checked rather than documented for the same reason the port range + # is: the names are all known at evaluation time, and the runtime + # symptom is a hive that looks healthy and reports no metrics, with + # nothing in any log naming the cause. + assertion = !lib.any (h: lib.elem h reservedOwners) (lib.attrNames hyperhiveCfg.swarm.hives); + message = '' + services.hyperhive.swarm.hives contains ${ + lib.concatMapStringsSep ", " (h: "'${h}'") ( + lib.filter (h: lib.elem h reservedOwners) (lib.attrNames hyperhiveCfg.swarm.hives) + ) + }, which the swarm collector reserves for its own pipelines. + + The collector names components `/` and uses the hive + name as the owner, so such a hive would silently replace the + swarm tier's parts and lose its own — it would keep accepting + pushes into a pipeline that routes nowhere. + + Rename the hive. Reserved: ${lib.concatMapStringsSep ", " (n: "'${n}'") reservedOwners}. + ''; + } { # A hive proves who it is with a token this provider mints, so # there is no version of this collector that runs without one. @@ -512,9 +554,9 @@ in # not have. Keeping it out of them makes the absence # structural rather than something to remember to strip. // lib.optionalAttrs (cfg.scrapeTargets != { }) { - "metrics/swarm" = { + "metrics/${swarmTierName}" = { receivers = [ "prometheus" ]; - processors = [ "resource/swarm" ]; + processors = [ "resource/${swarmTierName}" ]; exporters = exporterNames; }; }; @@ -602,8 +644,10 @@ in # an invented one (a sentinel, the local hive's name) would be # queried as though it meant something. // lib.optionalAttrs (cfg.scrapeTargets != { }) { - "resource/swarm".attributes = [ + "resource/${swarmTierName}".attributes = [ { + # The metric LABEL, a different namespace from the + # component name above — deliberately not interpolated. key = "swarm"; value = swarmDisplayName; action = "upsert";