Thirty-three module-eval fixtures were a second (or later) full `nixosSystem` evaluation of a config another fixture already evaluated: 16 shapes defined identically in two to seven cluster files, plus duplicates inside agent-queue-bao, core-toggle and swarm-services-switch. Each cluster imports ./lib.nix itself, so nothing was shared between them. nix/module-eval/shared.nix now defines each of those 16 shapes once. nix/checks.nix instantiates it a single time and passes it as `shared` to the 23 clusters that read one, and each cluster rebinds the fixture under its existing local name. Assertions are unchanged. Case text changes only in core-toggle, where two cases built the forge-host fixture inline and now read the file's `forgeHere`, and in two inline `let` bindings (bao-otel-collector, grafana) that now take the shared fixture. Six folds are value-equal rather than textually equal: they drop an explicit `deploy.hive-controller.enable = false` or `deploy.swarm-otel.enable = false` that equals what the option already resolves to for that config. Nothing under nix/host-modules sets either option apart from the `mkDefault allSwarmServices` on swarm-otel, and the only option-priority read there is `domain.highestPrio`. Part of #5171.
121 lines
4.8 KiB
Nix
121 lines
4.8 KiB
Nix
# `checks.module-eval-agent-otel` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
shared,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
agent
|
|
carriesJournaldSeverity
|
|
runGroup
|
|
;
|
|
|
|
# The log path's three hops, one fixture each. Nothing carries a journal
|
|
# record end to end at eval time, so what these defend is the part no tier
|
|
# can check for itself: each hop's output is the next hop's input, and
|
|
# every mismatch between them is silent — a push accepted and routed
|
|
# nowhere, a receiver pointed at an empty directory, a pipeline that does
|
|
# not exist.
|
|
agentBridge = "http://10.42.0.1:4318";
|
|
|
|
agentOtel = agent {
|
|
otel.enable = true;
|
|
otel.endpoint = agentBridge;
|
|
};
|
|
|
|
# The same agent over the other wire protocol. An exporter's NAME is what
|
|
# selects it, so this is where a defined exporter and the pipeline's
|
|
# reference to it can drift apart.
|
|
agentOtelGrpc = agent {
|
|
otel.enable = true;
|
|
otel.endpoint = agentBridge;
|
|
otel.protocol = "grpc";
|
|
};
|
|
|
|
agentNoOtel = shared.agentBare;
|
|
|
|
agentSettings = machine: machine.services.opentelemetry-collector.settings;
|
|
cases = [
|
|
{
|
|
# The journald receiver's own default directory is the RUNTIME
|
|
# journal, and a container that stores persistently leaves that
|
|
# empty. At the default the forwarder validates, starts, reports
|
|
# healthy and ships nothing, so this one literal is the difference
|
|
# between the path working and silently not.
|
|
name = "the agent forwarder reads the persistent journal, not the runtime one";
|
|
ok = ((agentSettings agentOtel).receivers.journald.directory or null) == "/var/log/journal";
|
|
}
|
|
{
|
|
# Wiring, not contents. A journald record arrives with its level in
|
|
# `PRIORITY` and nothing downstream reads that field, so a receiver with
|
|
# no severity parser ships every line to the log store labelled
|
|
# `Unspecified` — healthy, complete, and unqueryable by level.
|
|
#
|
|
# What this asks is only whether THIS receiver carries the shared
|
|
# mapping. ⚠️ Whether that mapping is RIGHT — the inverted direction,
|
|
# which is the part of it a reviewer cannot check by looking — is
|
|
# asserted once, in ./journald-severity.nix, against the file both tiers
|
|
# import. Re-asserting the table here would test that file twice and
|
|
# report one defect as two failures.
|
|
#
|
|
# The swarm collector's own receiver gets the sibling of this case in
|
|
# ./swarm-otel-core.nix. Deliberately NOT folded together with it: they
|
|
# cover different journals — this container's own, and the host's — and
|
|
# one tier quietly losing its parser while the other keeps one is
|
|
# precisely the half-fixed state worth catching.
|
|
#
|
|
# Membership rather than equality of the whole operator list, so a tier
|
|
# that later grows an unrelated operator of its own still passes.
|
|
name = "the agent forwarder's journald receiver carries the shared PRIORITY mapping";
|
|
ok = carriesJournaldSeverity (agentSettings agentOtel).receivers.journald;
|
|
}
|
|
{
|
|
# The hop's two ends: what it reads, and where what it reads goes.
|
|
# The endpoint is compared against the value the fixture handed the
|
|
# option rather than a literal spelled here, so an exporter that
|
|
# stopped reading the option fails instead of matching a constant
|
|
# that travelled beside it.
|
|
name = "the agent forwarder ships the journal to the endpoint its hive gave it";
|
|
ok =
|
|
let
|
|
s = agentSettings agentOtel;
|
|
p = s.service.pipelines.logs;
|
|
in
|
|
p.receivers == [ "journald" ]
|
|
&& p.exporters != [ ]
|
|
&& lib.all (e: (s.exporters ? ${e}) && s.exporters.${e}.endpoint == agentBridge) p.exporters;
|
|
}
|
|
{
|
|
# Presence control for the two cases above: with the switch off there
|
|
# is no collector in the container at all, so their passing is about
|
|
# the wiring rather than about a unit that renders regardless.
|
|
name = "an agent that has not opted into telemetry runs no collector";
|
|
ok = !agentNoOtel.services.opentelemetry-collector.enable;
|
|
}
|
|
{
|
|
# `otlp` and `otlphttp` are different components and the protocol
|
|
# option picks which one is defined. A pipeline left naming the other
|
|
# is a startup failure; an exporter no pipeline names is silence.
|
|
name = "the agent forwarder's exporter and its pipeline agree on the protocol";
|
|
ok =
|
|
let
|
|
s = agentSettings agentOtelGrpc;
|
|
in
|
|
(s.exporters ? otlp) && s.service.pipelines.logs.exporters == [ "otlp" ];
|
|
}
|
|
];
|
|
in
|
|
runGroup "agent-otel" cases
|