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
| Where | Command |
|---|---|
| One machine, with Docker or Podman | docker compose -f deploy/hub/compose.yaml up -d |
| A cluster, with Helm | helm install hub deploy/helm/ototo-hub --namespace ototo-hub --create-namespace |
| A single-node k3s, as we run it | kubectl apply -k deploy/k3s |
| Service | Port | What |
|---|---|---|
| collector | 4318 (HTTP), 4317 (gRPC) | OTLP in, from Otōto and Claude Code |
| grafana | 3000 | the dashboards. First login admin / admin, and Grafana then asks for a new password |
| prometheus | 9090 | queries 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 withkubectl -n ototo-hub port-forward svc/grafana 3000:3000. One release a namespace: the services have fixed names, which the configuration refers to.values.yamlhas 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 name | From | Labels worth knowing |
|---|---|---|
claude_code_cost_usage_USD_total | Claude Code | model, user_email, team, session_id |
claude_code_token_usage_tokens_total | Claude Code | type (input, output, cacheRead, cacheCreation), model, mcp_server_name |
claude_code_session_count_total, claude_code_active_time_seconds_total | Claude Code | |
ototo_calls_total | Otōto | tool, outcome, repo, model, endpoint |
ototo_local_tokens_total | Otōto | type, model, endpoint |
ototo_duration_seconds_bucket | Otōto | tool, delegated |
ototo_cost_usage_USD_total, ototo_fallbacks_total, ototo_claims_total, ototo_turns_total, ototo_reply_chars_total | Otō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/peopleprocessor 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.jsonorfleet.json, thensh deploy/sync.shto give the chart its copy.