Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The hub: usage across a team

The dashboard is one machine’s. The hub is where many machines’ counts meet: an OpenTelemetry Collector that Otōto and Claude Code both send to, Prometheus keeping what arrives, and Grafana with two dashboards. Only counts reach it, never questions, code or answers (Metrics).

It is three upstream images and their configuration, in deploy/ of the source. Nothing of Otōto’s runs in the hub, and nothing in Otōto needs one: any OpenTelemetry collector you already run will do, and the two dashboards can be loaded into a Grafana of your own.

What we run: the kustomize manifests, on a single k3s node, which is where our own machines report. The compose file and the Helm chart are the same configuration, and on 11 October 2026 both render clean (docker compose config, helm lint, helm template); we have not run those two for this page. Collector 0.161.0, Prometheus 3.15.0, Grafana 13.2.2.

What it shows

Otōto and Claude Code, Grafana’s home page, for a time range and a team:

  • At a glance: what Claude Code cost and how many tokens it used, Otōto’s calls, the small model’s tokens, what a paid fallback cost, and the calls that need a second look.
  • Claude Code: cost by model, tokens by type, Claude’s tokens by MCP server, and cost by team.
  • Otōto: calls by tool, the small model’s tokens by server, how long a delegated call takes (median and 90th percentile, by tool), outcomes, the claims in answers and how many were not backed, fallbacks, the tokens handed back to Claude, and the turns a delegated call takes.
  • People: usage person by person, and calls by person and repository.

Otōto fleet:

  • Right now: the servers that reported in the last half hour, the developers and machines behind them, the versions in use, how many are managed by an organisation, and who was seen this week.
  • What is running: by version, team and operating system, with the plugins loaded, where each server’s settings come from, and the model servers they use.
  • Over time, and a table of every server.

Run it

Clone the source for the files:

git clone https://gitlab.com/handmadedigital/projects/ototo.git && cd ototo
WhereCommand
One machine, with Docker or Podmandocker compose -f deploy/hub/compose.yaml up -d
A cluster, with Helmhelm install hub deploy/helm/ototo-hub --namespace ototo-hub --create-namespace
A single-node k3s, as we run itkubectl apply -k deploy/k3s
ServicePortWhat
collector4318 (HTTP), 4317 (gRPC)OTLP in, from Otōto and Claude Code
grafana3000the dashboards. First login admin / admin, and Grafana then asks for a new password
prometheus9090queries and raw series

Who can reach them differs, and matters, since Prometheus has no login:

  • Compose publishes the collector and Grafana on the machine, and Prometheus on 127.0.0.1 only.
  • The chart keeps all three inside the cluster (ClusterIP) until you say otherwise: --set collector.service.type=LoadBalancer, or an ingress of your own in front of 4318. Grafana is reached with kubectl -n ototo-hub port-forward svc/grafana 3000:3000. One release a namespace: the services have fixed names, which the configuration refers to. values.yaml has the images, Prometheus’s retention, the volumes’ sizes and storage class, and each service’s type and resources.
  • The kustomize manifests give all three a LoadBalancer, which on k3s binds the ports on the node itself: for a network you trust.

Prometheus keeps 30 days, 4 GB at most. Grafana’s volume holds its users and settings.

Point machines at it

Otōto, in ~/.config/ototo/config.toml:

otlp_endpoint = "http://<the hub>:4318"
otlp_attributes = "team=platform"

Claude Code, in the env block of ~/.claude/settings.json:

"env": {
  "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
  "OTEL_METRICS_EXPORTER": "otlp",
  "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
  "OTEL_EXPORTER_OTLP_ENDPOINT": "http://<the hub>:4318",
  "OTEL_RESOURCE_ATTRIBUTES": "team=platform"
}

For many machines, ototo managed --collector http://<the hub>:4318 --team platform writes both, as managed settings to push to each (For organisations).

Use the same team in both: the dashboard’s Team filter covers both sets of metrics. Claude Code puts the address of its Claude account (user.email) on every metric, and Otōto sends the same address, read from the account Claude Code is signed in with, so the By person table matches the two with no setting per person. user.email=<address> in otlp_attributes sends another, and user.email= none.

On a Mac, give the hub an IP address or a DNS name, not a .local one: where the host has no IPv6 address, macOS waits five seconds for one on every lookup of a .local name, which is as long as Otōto waits to connect. A line in /etc/hosts also cures it.

What arrives

Prometheus nameFromLabels worth knowing
claude_code_cost_usage_USD_totalClaude Codemodel, user_email, team, session_id
claude_code_token_usage_tokens_totalClaude Codetype (input, output, cacheRead, cacheCreation), model, mcp_server_name
claude_code_session_count_total, claude_code_active_time_seconds_totalClaude Code
ototo_calls_totalOtōtotool, outcome, repo, model, endpoint
ototo_local_tokens_totalOtōtotype, model, endpoint
ototo_duration_seconds_bucketOtōtotool, delegated
ototo_cost_usage_USD_total, ototo_fallbacks_total, ototo_claims_total, ototo_turns_total, ototo_reply_chars_totalOtōto

Resource attributes become labels too: job (ototo or claude-code), host_name and user_name (Otōto), user_email, user_account_uuid and organization_id (Claude Code), and anything set with team=…. Metrics has every metric Otōto sends, with its attributes.

Worth knowing before you rely on it

  • People are identifiable. E-mail addresses and account ids are kept, so that usage can be compared person by person. To report without them, add the attributes/people processor sketched in the collector’s configuration (deploy/k3s/collector/config.yaml): it deletes the address and hashes the account ids.
  • Nothing here is encrypted or behind a login, but Grafana. The collector takes plain HTTP, and Prometheus answers anyone who can reach it. On anything but a network you trust, keep the services inside the cluster and put an ingress with TLS, and whatever sign-in you use, in front of the collector and Grafana.
  • Every process counts from zero. Otōto runs a process a session, and Claude Code sends deltas, which the collector turns into running totals, so series come and go. The collector serves OpenMetrics, which carries each counter’s start time; Prometheus records a 0 there (created-timestamp-zero-ingestion), so that a process’s first call is counted, and the dashboards count exactly over a range, not by extrapolating (promql-extended-range-selectors). Both are set in the files here; a Prometheus of your own needs them too.
  • Events are not stored. Claude Code’s log events, if you turn them on, reach the collector’s log only. Add Loki to keep them.
  • The dashboards are files. An edit made in Grafana is lost on restart: export the JSON and replace deploy/k3s/grafana/ototo.json or fleet.json, then sh deploy/sync.sh to give the chart its copy.