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;
|
||||
};
|
||||
|
||||
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 {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
|
|
@ -722,6 +781,11 @@ 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
|
||||
|
|
@ -1640,6 +1704,34 @@ in
|
|||
systemd.services.hive-ag3nt =
|
||||
let
|
||||
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
|
||||
{
|
||||
description = "${binary} harness";
|
||||
|
|
@ -1662,9 +1754,10 @@ in
|
|||
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
|
||||
# bind-mounts and gateway upstream config stay in sync.
|
||||
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
||||
};
|
||||
}
|
||||
// otelEnv;
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
||||
ExecStart = if otel.enable then "${otelExecStart}" else "${pkgs.hyperhive}/bin/${binary} serve";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 2;
|
||||
# Per-service runtime dir owned by `User=` below; the harness
|
||||
|
|
@ -1674,6 +1767,9 @@ in
|
|||
RuntimeDirectory = "hive-config";
|
||||
User = userName;
|
||||
Group = userName;
|
||||
}
|
||||
// lib.optionalAttrs (otel.enable && otel.headersCredential != null) {
|
||||
LoadCredential = [ "otel-headers:${otel.headersCredential}" ];
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue