fix(swarm-otel): reserve the swarm tier's component names from hive names

The collector names components `<kind>/<owner>` — 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.
This commit is contained in:
atlas 2026-08-24 13:20:31 +02:00
commit 4de4878e74

View file

@ -43,6 +43,21 @@ let
autheliaCfg = hyperhiveCfg.swarm.authelia;
# The collector names its components `<kind>/<owner>`, where `<owner>` 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 `<owner>` 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/<name>` pipeline
# disappears, and its `resource/<name>` 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 `<kind>/<owner>` 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";