# `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, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) agent 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 = agent { }; 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"; } { # 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