metrics to exponentials for local claude

This commit is contained in:
müde 2026-07-31 19:58:18 +02:00
commit ac5ea6e5e4
3 changed files with 132 additions and 3 deletions

View file

@ -0,0 +1,87 @@
{
config,
lib,
osConfig,
pkgs,
...
}:
let
claude-code = pkgs.unstable.claude-code;
# Endpoint and wire protocol come straight off the hive's own OTLP config in
# nixosModules/constellation-swarm.nix, so there is one place to change them.
# Only those two are shared: the hive's `agent=` / `hive=` / `swarm=` resource
# labels are deliberately not carried over.
hiveOtel = osConfig.services.hyperhive.otel;
# Written by the `claude-otel-user-header` oneshot in that same module, which
# copies the hive's root-only auth header into /run owned by this user.
# Absent on hosts without `my.constellation-swarm`, where the wrapper starts
# claude with no telemetry at all rather than retrying an unauthenticated
# export every interval.
headerFile = "${osConfig.my.constellation-swarm.userOtelHeaderDir}/${config.home.username}";
# OTEL's `host.arch` is an enum of its own spelling, not uname's.
hostArch =
{
x86_64-linux = "amd64";
aarch64-linux = "arm64";
}
.${pkgs.stdenv.hostPlatform.system} or pkgs.stdenv.hostPlatform.uname.processor;
# `deployment.environment.name` is what keeps these interactive sessions
# apart from the agent fleet in the same backend, now that the hive's own
# identifying labels are gone.
resourceAttributes = lib.concatStringsSep "," [
"service.name=claude-code"
"host.arch=${hostArch}"
"os.type=linux"
"deployment.environment.name=workstation"
];
# Env claude-code reads to export telemetry. Set by the wrapper rather than
# written into ~/.claude/settings.json, because that file is self-mutating —
# /model, /config and plugin toggles all write to it, so home-manager cannot
# own it without breaking them.
otelEnv = {
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
OTEL_METRICS_EXPORTER = "otlp";
# Interactive sessions carry far more sensitive content than hive agents,
# so prompt events and tool-decision records never leave the machine.
OTEL_LOGS_EXPORTER = "none";
OTEL_TRACES_EXPORTER = "none";
OTEL_EXPORTER_OTLP_PROTOCOL = hiveOtel.protocol;
OTEL_EXPORTER_OTLP_ENDPOINT = hiveOtel.endpoint;
# claude-code defaults to DELTA, which Prometheus/Mimir-family backends
# silently drop without a deltatocumulative processor.
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative";
OTEL_METRICS_INCLUDE_VERSION = "1";
}
# Follow the hive's export cadence too, so retuning it stays a one-line
# change over in constellation-swarm.nix. Null there leaves claude-code's
# own 60s default. `debug` is pointedly not mirrored: the OTEL SDK's stderr
# diagnostics would scribble over an interactive TUI.
// lib.optionalAttrs (hiveOtel.metricIntervalMs != null) {
OTEL_METRIC_EXPORT_INTERVAL = toString hiveOtel.metricIntervalMs;
};
exports = lib.concatStrings (
lib.mapAttrsToList (name: value: " export ${name}=${lib.escapeShellArg value}\n") otelEnv
);
# Shadows claude-code's own `bin/claude`, so claude-code itself is kept out
# of home.packages (see ./default.nix) — two derivations installing the same
# binary is a home-manager collision, not a precedence win.
claude-wrapper = pkgs.writeShellScriptBin "claude" ''
hdr=${lib.escapeShellArg headerFile}
if [ -r "$hdr" ]; then
${exports} export OTEL_EXPORTER_OTLP_HEADERS="$(cat "$hdr")"
export OTEL_RESOURCE_ATTRIBUTES=${lib.escapeShellArg resourceAttributes}",host.name=$(${pkgs.coreutils}/bin/uname -n)"
fi
exec ${claude-code}/bin/claude "$@"
'';
in
{
home.packages = [ claude-wrapper ];
}

View file

@ -2,6 +2,7 @@
{
imports = [
# keep-sorted start
./claude.nix
./editorconfig.nix
./element.nix
./fonts.nix
@ -90,7 +91,6 @@
tea
telegram-desktop
thunderbird
unstable.claude-code
vlc
wireguard-tools
wirelesstools

View file

@ -7,11 +7,26 @@
}:
let
cfg = config.my.constellation-swarm;
adminUsers = [ "muede" ];
otelCredential = "/etc/hyperhive/otel-cred";
runtimeDirName = "claude-otel";
in
{
options.my.constellation-swarm = {
enable = lib.mkEnableOption "hyperhive / constellation swarm";
hiveName = lib.mkOption { type = lib.types.str; };
userOtelHeaderDir = lib.mkOption {
type = lib.types.str;
default = "/run/${runtimeDirName}";
readOnly = true;
description = ''
Directory holding the per-admin copies of the hive's OTLP auth header,
one 0400 file per user named after them. Read-only so the home-manager
side (homeConfigurations/muede/claude.nix) can point at it via
`osConfig` instead of repeating the path.
'';
};
};
imports = [ hyperhive.nixosModules.default ];
@ -39,12 +54,39 @@ in
otel = {
enable = true;
endpoint = "https://exponentials.vibec0re.mov/otel";
headersCredential = "/etc/hyperhive/otel-cred";
headersCredential = otelCredential;
};
c0re = {
adminUsers = ["muede"];
inherit adminUsers;
agentMemoryMax = "6G";
claudeCodePackage = pkgs.unstable.claude-code;
};
};
# `otelCredential` is root:root 0600, so the interactive `claude` an admin
# runs as themselves cannot read it — only the hive's agent units can, via
# LoadCredential. Hand each admin their own copy so the wrapper in
# homeConfigurations/muede/claude.nix can pick it up. It lands in /run
# (tmpfs) rather than the nix store because the store is world-readable, and
# rather than /etc because the header should not outlive a reboot.
config.systemd.services.claude-otel-user-header = lib.mkIf cfg.enable {
description = "Expose the hive OTLP auth header to interactive admin users";
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = otelCredential;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
RuntimeDirectory = runtimeDirName;
# Traversable by everyone; the per-user files inside are the 0400 part.
RuntimeDirectoryMode = "0755";
ExecStart = pkgs.writeShellScript "claude-otel-user-header" ''
set -eu
umask 077
for user in ${lib.escapeShellArgs adminUsers}; do
${pkgs.coreutils}/bin/install -m0400 -o "$user" -g root \
${lib.escapeShellArg otelCredential} "$RUNTIME_DIRECTORY/$user"
done
'';
};
};
}