feat(#1930): move otel stats export to host-level services.hyperhive.otel

This commit is contained in:
damocles 2026-06-23 20:42:15 +02:00 committed by mara
commit 838cc9af9a
3 changed files with 303 additions and 38 deletions

View file

@ -187,6 +187,75 @@ in
'';
};
# Hive-wide OTEL stats export. Set ONCE here at host level; the
# meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the
# HYPERHIVE_OTEL_* env exported below off hive-c0re's unit and injects
# the matching `hyperhive.otel.*` build-time config into EVERY agent
# (mirroring the CA-cert injection), so each agent's harness exports
# its own Claude Code stats directly to the collector. There is no
# per-agent opt-in — this is the single switch for the whole hive.
options.services.hyperhive.otel = {
enable = lib.mkEnableOption ''
hive-wide export of every agent's Claude Code stats (token usage,
cost, tool calls) to an OTLP endpoint via Claude Code's built-in
OpenTelemetry. One switch for all agents; each harness exports
directly to the collector, so it keeps working even when hive-c0re
is down
'';
endpoint = lib.mkOption {
type = lib.types.str;
default = "";
example = "https://collector.example.com/otel";
description = ''
OTLP collector endpoint, set as `OTEL_EXPORTER_OTLP_ENDPOINT`
for every agent. Required when `enable` is true.
'';
};
protocol = lib.mkOption {
type = lib.types.enum [
"http/protobuf"
"http/json"
"grpc"
];
default = "http/protobuf";
description = ''
OTLP wire protocol, set as `OTEL_EXPORTER_OTLP_PROTOCOL`.
'';
};
headersCredential = lib.mkOption {
# `str`, not `path`: a `path`-typed relative literal is hash-copied
# into the world-readable nix store at eval time, defeating the
# point. Keep it a string + require an absolute runtime path so the
# secret is only ever read from disk by systemd at start.
type = lib.types.nullOr lib.types.str;
default = null;
example = "/run/secrets/otel-headers";
description = ''
Absolute path to an operator-provided secret file whose contents
become `OTEL_EXPORTER_OTLP_HEADERS` (e.g.
`Authorization=Bearer <token>`). Loaded via systemd
`LoadCredential` into each agent's unit-private credential store
at runtime, so the token is never copied into the nix store or
exposed in argv. Must be absolute. Leave null if the endpoint
needs no auth header.
'';
};
extraResourceAttributes = lib.mkOption {
type = lib.types.str;
default = "";
example = "deployment.environment=prod";
description = ''
Extra comma-separated entries appended to
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
`service.name` / `agent` / `hive` / `swarm` labels.
'';
};
};
# Peer hives in the same swarm. Each entry declares a remote hive
# reachable from this host. Serialised to JSON and injected as
# `HYPERHIVE_PEERS` into the hive-c0re service and forwarded to agent
@ -720,24 +789,31 @@ in
config.services.hyperhive.swarm.wireguard.listenPort
];
assertions = lib.mkIf config.services.hyperhive.swarm.wireguard.enable [
{
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
Generate a key: wg genkey > /etc/wireguard/hive.key
'';
}
{
assertion = config.services.hyperhive.swarm.wireguard.address != "";
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.address to be set
(e.g. "10.100.0.1/24").
'';
}
];
assertions =
lib.optionals config.services.hyperhive.swarm.wireguard.enable [
{
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
Generate a key: wg genkey > /etc/wireguard/hive.key
'';
}
{
assertion = config.services.hyperhive.swarm.wireguard.address != "";
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.address to be set
(e.g. "10.100.0.1/24").
'';
}
]
++ lib.optionals config.services.hyperhive.otel.enable [
{
assertion = config.services.hyperhive.otel.endpoint != "";
message = "services.hyperhive.otel.enable is true but services.hyperhive.otel.endpoint is empty.";
}
];
systemd.services.hive-c0re = {
description = "hyperhive coordinator daemon";
@ -790,6 +866,26 @@ in
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
}
// lib.optionalAttrs config.services.hyperhive.otel.enable (
# Hive-wide OTEL config -> read by meta.rs::otel_config and
# injected as build-time `hyperhive.otel.*` into every agent.
# Endpoint presence is the enable signal on the meta side; the
# optional fields are only emitted when set so absent values
# don't render no-op env lines.
let
otel = config.services.hyperhive.otel;
in
{
HYPERHIVE_OTEL_ENDPOINT = otel.endpoint;
HYPERHIVE_OTEL_PROTOCOL = otel.protocol;
}
// lib.optionalAttrs (otel.extraResourceAttributes != "") {
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
}
// lib.optionalAttrs (otel.headersCredential != null) {
HYPERHIVE_OTEL_HEADERS_CREDENTIAL = otel.headersCredential;
}
)
// {
# In-cluster forge URL — the gateway vhost (`forge.<domain>`), which
# nginx proxies to forgejo. The forge is mandatory, so this is

View file

@ -174,23 +174,34 @@ in
visible = false;
};
# OTEL stats export is configured ONCE at host level via
# `services.hyperhive.otel.*` (see nix/modules/hive-c0re.nix) and
# injected into every agent's build by the meta-flake renderer
# (`hive-c0re/src/meta.rs::otel_config`). These per-agent options are
# the build-time implementation surface that injection writes into;
# they are not meant to be set directly in an agent.nix. Marked
# `internal` so the host option is the only documented operator knob.
options.hyperhive.otel = {
enable = lib.mkEnableOption ''
exporting this agent's Claude Code stats (token usage, cost, tool
calls) to an OTLP endpoint via Claude Code's built-in OpenTelemetry.
Each agent's harness exports its own stats directly to the collector,
so it keeps working even when hive-c0re is down. Meant to be enabled
hive-wide (one switch for every agent) - there is no per-agent
opt-in flag beyond this option
'';
enable = lib.mkOption {
type = lib.types.bool;
default = false;
internal = true;
description = ''
Export this agent's Claude Code stats (token usage, cost, tool
calls) to an OTLP endpoint via Claude Code's built-in
OpenTelemetry. Each agent's harness exports directly to the
collector, so it keeps working even when hive-c0re is down.
Host-driven: set `services.hyperhive.otel.enable` instead.
'';
};
endpoint = lib.mkOption {
type = lib.types.str;
default = "";
example = "https://collector.example.com/otel";
internal = true;
description = ''
OTLP collector endpoint, set as `OTEL_EXPORTER_OTLP_ENDPOINT`.
Required when `enable` is true.
Host-driven via `services.hyperhive.otel.endpoint`.
'';
};
@ -201,8 +212,10 @@ in
"grpc"
];
default = "http/protobuf";
internal = true;
description = ''
OTLP wire protocol, set as `OTEL_EXPORTER_OTLP_PROTOCOL`.
Host-driven via `services.hyperhive.otel.protocol`.
'';
};
@ -214,27 +227,27 @@ in
# is only ever read from disk by systemd at start, never nix-stored.
type = lib.types.nullOr lib.types.str;
default = null;
example = "/run/secrets/otel-headers";
internal = true;
description = ''
Absolute path to an operator-provided secret file whose contents
become `OTEL_EXPORTER_OTLP_HEADERS` (e.g.
`Authorization=Bearer <token>`). Loaded via systemd
`LoadCredential` into the unit-private credential store at
runtime, so the token is never copied into the nix store or
exposed in the process argv. Must be an absolute path (systemd
`LoadCredential` requires one). Leave null if the endpoint needs
no auth header.
exposed in the process argv. Host-driven via
`services.hyperhive.otel.headersCredential`.
'';
};
extraResourceAttributes = lib.mkOption {
type = lib.types.str;
default = "";
example = "deployment.environment=prod";
internal = true;
description = ''
Extra comma-separated entries appended to
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
`service.name` / `agent` / `hive` / `swarm` labels.
Host-driven via `services.hyperhive.otel.extraResourceAttributes`.
'';
};
};
@ -787,11 +800,6 @@ in
'';
assertions = [
# OTEL export needs an endpoint to point at.
{
assertion = !config.hyperhive.otel.enable || config.hyperhive.otel.endpoint != "";
message = "hyperhive.otel.enable is true but hyperhive.otel.endpoint is empty.";
}
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
# expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`.
# If `self` leaks into flakeInputs the agent gets a spurious attrset