feat(#1930): optional per-agent OTEL stats export via claude-code telemetry
This commit is contained in:
parent
4c53898382
commit
97ef00742e
1 changed files with 98 additions and 2 deletions
|
|
@ -174,6 +174,65 @@ in
|
||||||
visible = false;
|
visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
|
||||||
|
endpoint = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "https://collector.example.com/otel";
|
||||||
|
description = ''
|
||||||
|
OTLP collector endpoint, set as `OTEL_EXPORTER_OTLP_ENDPOINT`.
|
||||||
|
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 {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/run/secrets/otel-headers";
|
||||||
|
description = ''
|
||||||
|
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. 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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
options.hyperhive.allowedRecipients = lib.mkOption {
|
options.hyperhive.allowedRecipients = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
|
@ -722,6 +781,11 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
assertions = [
|
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
|
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
|
||||||
# expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`.
|
# expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`.
|
||||||
# If `self` leaks into flakeInputs the agent gets a spurious attrset
|
# If `self` leaks into flakeInputs the agent gets a spurious attrset
|
||||||
|
|
@ -1640,6 +1704,34 @@ in
|
||||||
systemd.services.hive-ag3nt =
|
systemd.services.hive-ag3nt =
|
||||||
let
|
let
|
||||||
binary = "hive";
|
binary = "hive";
|
||||||
|
otel = config.hyperhive.otel;
|
||||||
|
# Claude Code's native OpenTelemetry is env-driven; the harness
|
||||||
|
# spawns `claude` as a child which inherits this unit's env, so
|
||||||
|
# setting these here is all it takes to export per-agent stats.
|
||||||
|
otelEnv = lib.optionalAttrs otel.enable {
|
||||||
|
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
||||||
|
OTEL_METRICS_EXPORTER = "otlp";
|
||||||
|
OTEL_LOGS_EXPORTER = "otlp";
|
||||||
|
OTEL_EXPORTER_OTLP_PROTOCOL = otel.protocol;
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT = otel.endpoint;
|
||||||
|
};
|
||||||
|
# When OTEL is on, wrap the harness launch so (1) the bearer-token
|
||||||
|
# header is read from the systemd credential at start (never in the
|
||||||
|
# nix store or argv) and (2) the resource attributes are assembled
|
||||||
|
# from this agent's name (known at build time) plus the hive/swarm
|
||||||
|
# names (inherited HYPERHIVE_HIVE_NAME / HYPERHIVE_SWARM_NAME env,
|
||||||
|
# the same vars `identity::hive_name`/`swarm_name` read at runtime).
|
||||||
|
otelExecStart = pkgs.writeShellScript "hive-serve-otel" ''
|
||||||
|
set -eu
|
||||||
|
if [ -n "''${CREDENTIALS_DIRECTORY:-}" ] && [ -r "$CREDENTIALS_DIRECTORY/otel-headers" ]; then
|
||||||
|
OTEL_EXPORTER_OTLP_HEADERS="$(cat "$CREDENTIALS_DIRECTORY/otel-headers")"
|
||||||
|
export OTEL_EXPORTER_OTLP_HEADERS
|
||||||
|
fi
|
||||||
|
export OTEL_RESOURCE_ATTRIBUTES="service.name=hyperhive-agent,agent=${userName},hive=''${HYPERHIVE_HIVE_NAME:-unknown},swarm=''${HYPERHIVE_SWARM_NAME:-unknown}${
|
||||||
|
lib.optionalString (otel.extraResourceAttributes != "") ",${otel.extraResourceAttributes}"
|
||||||
|
}"
|
||||||
|
exec ${pkgs.hyperhive}/bin/${binary} serve
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
description = "${binary} harness";
|
description = "${binary} harness";
|
||||||
|
|
@ -1662,9 +1754,10 @@ in
|
||||||
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
|
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
|
||||||
# bind-mounts and gateway upstream config stay in sync.
|
# bind-mounts and gateway upstream config stay in sync.
|
||||||
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
||||||
};
|
}
|
||||||
|
// otelEnv;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
ExecStart = if otel.enable then "${otelExecStart}" else "${pkgs.hyperhive}/bin/${binary} serve";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 2;
|
RestartSec = 2;
|
||||||
# Per-service runtime dir owned by `User=` below; the harness
|
# Per-service runtime dir owned by `User=` below; the harness
|
||||||
|
|
@ -1674,6 +1767,9 @@ in
|
||||||
RuntimeDirectory = "hive-config";
|
RuntimeDirectory = "hive-config";
|
||||||
User = userName;
|
User = userName;
|
||||||
Group = userName;
|
Group = userName;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (otel.enable && otel.headersCredential != null) {
|
||||||
|
LoadCredential = [ "otel-headers:${otel.headersCredential}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue