diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 2a834850..2f716f80 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -1260,6 +1260,15 @@ where # `hyperhive.user.name` match the agent's identity # instead of the harness default of `"agent"`. hyperhive.user.name = name; + # Without this an agent container is called `nixos` and ships + # every log line under that: nixpkgs sets the hostname from the + # attr name in the `containers.` submodule, and an + # agent's system is its own `nixosSystem` off this flake, which + # never evaluates that option. The machine name, not the + # logical one — it is what `container.name` on our metrics and + # nspawn itself already call this container, so logs join to + # both without a prefix transform. + networking.hostName = "h-${name}"; hyperhive.claudeMemoryMaxBytes = memoryMaxBytes; programs.git.config.user = { name = name; @@ -1760,6 +1769,42 @@ mod tests { ); } + #[test] + fn render_flake_names_each_agent_container_after_the_agent() { + let out = render_flake( + "github:example/hyperhive", + "", + "path:/nix/store/aaaa-nixpkgs-source", + None, + 8000, + "she/her", + &std::collections::HashMap::new(), + "4G", + &[sample_spec("alice", false, 9001)], + ); + assert!( + out.contains(r#"networking.hostName = "h-${name}";"#), + "an agent container with no hostname of its own is called `nixos`, \ + and ships every log line under that:\n{out}" + ); + // The machine name `nixos-container` and our own metrics already use, + // so a log line joins to a metric without a prefix transform. + assert!( + !out.contains("networking.hostName = name;"), + "must be the machine name, not the logical one:\n{out}" + ); + // Inside `mkAgent`'s module block, so it applies to every agent and + // resolves to that agent's name — not emitted once at the top level. + let mk = out.find("mkAgent = {").expect("mkAgent block"); + let host = out + .find(r#"networking.hostName = "h-${name}";"#) + .expect("hostname line"); + assert!( + host > mk, + "hostname must be emitted inside the per-agent module block:\n{out}" + ); + } + #[test] fn render_flake_pins_claude_path_without_adding_an_input() { // The host-pinned claude travels as a bare store path assigned to diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index f3294def..5856b39f 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -1108,11 +1108,9 @@ in # # Attribution is journald's either way — these fields are # written by it rather than by the logging process. ⚠️ Use - # `_MACHINE_ID` to tell machines apart, not `_HOSTNAME`: an - # agent container reports the NixOS default `nixos`. Whether - # every container does, and why the `networking.hostName` - # nixos-containers sets does not reach them, is unresolved — - # so rely on the field that is per-machine by construction. + # `_MACHINE_ID` to tell machines apart, not `_HOSTNAME`: a + # hostname is a config value two machines can share, where a + # machine id is per-machine by construction. // lib.optionalAttrs collectLogs { journald = { directory = hostJournalDir; @@ -1162,8 +1160,7 @@ in # `_stream_fields` is one stream per unit per machine. # `_MACHINE_ID` is what makes that true — it is per-machine by # construction, where `_HOSTNAME` is only as distinct as the - # hostnames happen to be, and an agent container's is the - # NixOS default `nixos`. Keying on the hostname alone merges + # hostnames happen to be. Keying on the hostname alone merges # the streams of any two machines sharing one, which is what # the host and containerised `opentelemetry-collector.service` # would do. diff --git a/nix/module-eval.nix b/nix/module-eval.nix index acaf5b70..5b948882 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -1459,9 +1459,9 @@ let && lib.hasInfix "logs.t.local" l; } { - # `_HOSTNAME` cannot separate containers: none sets - # `networking.hostName`, so it is `nixos` for all of them and every - # container's stream for a unit name merges into one. + # `_HOSTNAME` cannot separate machines on its own: a hostname is a + # config value two of them can share, and then every stream for a unit + # name merges into one. name = "the log stream is keyed by machine, not only by a hostname every container shares"; ok = let