Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Claude Code Telemetry — System Reference

End-to-end, self-hosted OpenTelemetry pipeline for monitoring Claude Code usage across an organization (~100 employees in the reference deployment). Everything runs on your own infrastructure — prompts and metrics never leave your network.

Built and maintained by Space-O Technologies; open-sourced for anyone who wants to run the same stack. This README is the single canonical doc — read it top to bottom and you'll know everything. Other docs in docs/ are deep dives.

Configuration: hostnames below use the placeholder telemetry.example.com and the repo placeholder YOUR-ORG/claude-code-optel. Replace both with your own host and repo. The deployment URL is set per-install via the OTEL_BASE_URL / PUBLIC_BASE_URL environment variables (see server/env.template).


Getting Started (self-host quick start)

Stand up the full stack and create your two kinds of admin access — Grafana's admin login and the gateway admin UI — in a few minutes.

Prerequisites

  • A host with Docker + Docker Compose.
  • A reverse proxy with TLS (Apache or nginx) in front, serving the stack under a path prefix (default /projects/optel/). The Docker services bind to 127.0.0.1 only — the proxy is the sole public entrypoint. A ready-to-include Apache config is at server/apache-optel.conf (the server/devops-setup.sh script wires it into an existing vhost). openssl for generating the token.

Steps

# 1. Configure
cd server
cp env.template .env

# 2. Generate the gateway admin token and put it in .env (ADMIN_TOKEN=…)
openssl rand -hex 32
$EDITOR .env
#    Set in .env:
#      PUBLIC_BASE_URL=https://<your-host>/projects/optel
#      ADMIN_TOKEN=<the openssl output>
#      GRAFANA_ADMIN_PASSWORD=<a strong password, NO '$' characters>

# 3. Bring up the stack
docker compose --env-file .env up -d

Then point your reverse proxy at the stack (see server/apache-optel.conf) so https://<your-host>/projects/optel/ reaches Grafana and the gateway.

Create admin access

1) Grafana admin (the dashboard login). There is no signup — the first admin is seeded from your .env:

URL https://<your-host>/projects/optel/
Username admin
Password the GRAFANA_ADMIN_PASSWORD you set in .env

GF_SECURITY_ADMIN_PASSWORD is only read on first boot. Log in, then change the password in Administration → Users; the .env value is irrelevant after that.

2) Gateway admin UI (user management). This UI has no account — it's gated by the shared ADMIN_TOKEN from your .env:

URL https://<your-host>/projects/optel/install/admin.html
Auth paste your ADMIN_TOKEN into the token box

Enroll the first user

From the admin UI, paste one or more emails and click Add users to mint per-user ingest tokens. CLI equivalent on the host:

docker compose exec gateway python /app/mint_token.py mint alice@example.com

Employees then self-install from https://<your-host>/projects/optel/install/.

Security note: ADMIN_TOKEN grants full user-management access and GRAFANA_ADMIN_PASSWORD is your dashboard root login — keep .env out of git (it's already in .gitignore) and treat both as secrets.


1. What it does

Thing Where
Captures Claude Code telemetry from every employee's laptop Built-in OTel SDK in Claude Code itself, enabled via ~/.claude/settings.json
Sends to your private server (encrypted, authenticated, per-user) Internal Apache → custom auth gateway → OTel collector
Stores 90 days of metrics + full prompt content VictoriaMetrics + VictoriaLogs (self-hosted)
Lets admins browse via dashboards + filter prompts Grafana on the same server
Admins manage who has access Web admin UI (no SSH needed)

Nothing leaves your network. Anthropic doesn't see this — they only see the original API request from Claude Code, exactly the same as without telemetry.


2. Reference deployment

The reference deployment runs behind an existing Apache reverse proxy on a single VM, served under a path prefix. Substitute your own values:

Server telemetry.example.com (any VM with Docker + a reverse proxy)
Public URL https://telemetry.example.com/projects/optel/
TLS Terminated at the reverse proxy (Apache cert in the reference setup)
Repo https://github.com/YOUR-ORG/claude-code-optel
CI/CD CI job watches main, runs docker compose up -d --build + restart (reference setup uses Jenkins)
Backup Docker named volumes (gateway-data, vm-data, vl-data, grafana-data) — automate this yourself

3. Public URLs employees and admins use

For URL
Employee install https://telemetry.example.com/projects/optel/install/
Admin user mgmt https://telemetry.example.com/projects/optel/install/admin.html
Grafana https://telemetry.example.com/projects/optel/
Install version JSON (machine-readable) https://telemetry.example.com/projects/optel/install/version.json

4. Architecture

┌───────────────────────────────────────────────────────────────────────────┐
│                                                                           │
│  EMPLOYEE LAPTOPS (~100)                                                  │
│  ┌──────────────────────────────────────────────────┐                     │
│  │ Claude Code (OTel SDK enabled)                   │                     │
│  │  ~/.claude/settings.json: OTEL_*, INGEST_TOKEN   │                     │
│  │  ~/.zshrc claude() wrapper: adds                 │                     │
│  │     claude.project_dir, claude.project_name,     │                     │
│  │     claude.git_branch, claude.git_repo           │                     │
│  └────────────────────┬─────────────────────────────┘                     │
│                       │ HTTPS POST OTLP/protobuf                          │
│                       │ Authorization: Bearer <per-user-token>            │
│                       ▼                                                   │
│  ┌──────────────────────────────────────────────────────────────────┐     │
│  │  telemetry.example.com                                                  │     │
│  │                                                                  │     │
│  │  Apache (existing, fronts the host's /projects/*)                    │     │
│  │  ├── /projects/optel/install/*       static files (HTML, scripts)│     │
│  │  ├── /projects/optel/register        →  127.0.0.1:18888 (gateway)│     │
│  │  ├── /projects/optel/admin/*         →  127.0.0.1:18888 (gateway)│     │
│  │  ├── /projects/optel/v1/*            →  127.0.0.1:18888 (gateway)│     │
│  │  ├── /projects/optel/api/live/* (WS) →  127.0.0.1:13000 (grafana)│     │
│  │  └── /projects/optel/*               →  127.0.0.1:13000 (grafana)│     │
│  │                                                                  │     │
│  │  ┌──────────────────────────────────────────────────────────┐    │     │
│  │  │ Docker stack (all bound to 127.0.0.1 only)               │    │     │
│  │  │                                                          │    │     │
│  │  │  gateway (Python/Flask)  port 18888                      │    │     │
│  │  │     ├── /register   email→token (with confirm flow)      │    │     │
│  │  │     ├── /admin/*    user mgmt (ADMIN_TOKEN auth)         │    │     │
│  │  │     ├── /v1/*       OTLP proxy: validate token + email,  │    │     │
│  │  │     │               log mismatches, forward to collector │    │     │
│  │  │     └── SQLite (tokens table + mismatches table)         │    │     │
│  │  │                          │                               │    │     │
│  │  │                          ▼                               │    │     │
│  │  │  otelcol (otel/opentelemetry-collector-contrib:0.114.0)  │    │     │
│  │  │     port 14318 (host) / 4318 (container)                 │    │     │
│  │  │     Pipelines:                                           │    │     │
│  │  │      metrics: otlp → memlimit → resource → delta2cum     │    │     │
│  │  │               → batch → prometheusremotewrite ──┐        │    │     │
│  │  │      logs:    otlp → memlimit → attr/cleanup    │        │    │     │
│  │  │               → resource → batch                │        │    │     │
│  │  │               → otlphttp/vlogs ──┐              │        │    │     │
│  │  │                                  │              │        │    │     │
│  │  │                  ┌───────────────┘              │        │    │     │
│  │  │                  ▼                              ▼        │    │     │
│  │  │  vlogs (VictoriaLogs :latest)        vmsingle (VM 1.106) │    │     │
│  │  │     /select/logsql/query                /api/v1/write    │    │     │
│  │  │     Stream fields:                      Series labels:   │    │     │
│  │  │      service.name, user.email,           service_name,   │    │     │
│  │  │      terminal.type                       user_email,     │    │     │
│  │  │                                          model, etc.     │    │     │
│  │  │                                                          │    │     │
│  │  │  grafana (grafana/grafana:11.3.1)  port 13000            │    │     │
│  │  │     Datasources (auto-provisioned):                      │    │     │
│  │  │      VictoriaMetrics → vmsingle (Prometheus)             │    │     │
│  │  │      VictoriaLogs → vlogs (VL native plugin)             │    │     │
│  │  │     Dashboards (auto-provisioned from /dashboards/):     │    │     │
│  │  │      Overview, Per-user, Models & Efficiency,            │    │     │
│  │  │      Prompt Browser                                      │    │     │
│  │  └──────────────────────────────────────────────────────────┘    │     │
│  └──────────────────────────────────────────────────────────────────┘     │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

5. Components and what they do

5.1 The Claude Code laptop side

Two things are added to each employee's machine:

A. ~/.claude/settings.json env block (13 OTEL_* keys):

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER":          "otlp",
    "OTEL_LOGS_EXPORTER":             "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL":    "http/protobuf",
    "OTEL_EXPORTER_OTLP_ENDPOINT":    "https://telemetry.example.com/projects/optel",
    "OTEL_EXPORTER_OTLP_HEADERS":     "Authorization=Bearer <per-user-token>",
    "OTEL_LOG_USER_PROMPTS":          "1",
    "OTEL_METRIC_EXPORT_INTERVAL":    "60000",
    "OTEL_LOGS_EXPORT_INTERVAL":      "5000",
    "OTEL_METRICS_INCLUDE_SESSION_ID": "false",
    "OTEL_METRICS_INCLUDE_VERSION":    "false",
    "OTEL_METRICS_INCLUDE_ACCOUNT_UUID": "false",
    "OTEL_RESOURCE_ATTRIBUTES":       "claude.install_version=4"
  }
}

B. Shell wrapper in ~/.zshrc / ~/.bashrc (or PowerShell $PROFILE):

claude() {
  local extra="claude.project_dir=${PWD},claude.project_name=$(basename "${PWD}")"
  if [in git repo]; then
    extra="${extra},claude.git_branch=$(git symbolic-ref --short HEAD),claude.git_repo=$(basename ...)"
  fi
  OTEL_RESOURCE_ATTRIBUTES="${OTEL_RESOURCE_ATTRIBUTES:+...,}${extra}" command claude "$@"
}

This wrapper tags every claude invocation with the current working directory and git context, without modifying Claude Code itself.

5.2 The gateway (Python Flask, gateway/app.py)

The single most important custom piece. Lives at 127.0.0.1:18888 inside Docker.

Three responsibilities:

  1. POST /register — install-page email→token lookup

    • First call (status=pending): returns token immediately
    • First call (status=confirmed): returns requires_confirmation: true WITHOUT token
    • Second call with confirm_reinstall: true: returns token (multi-device flow)
    • Revoked: returns 410. Email not on list: returns 404.
  2. POST /v1/{logs,metrics} — OTLP proxy with auth + identity binding

    • Validates Authorization: Bearer against tokens table
    • Parses incoming OTLP/protobuf payload to extract user.email attribute
    • Compares to email the token was issued for — rejects with 403 if mismatch
    • Logs every mismatch to mismatches table for admin audit
    • Marks token confirmed on first valid match
    • Forwards valid traffic to otelcol:4318
  3. GET/POST/DELETE /admin/* — admin operations (require ADMIN_TOKEN)

    • GET /admin/users — list everyone with state
    • POST /admin/users — add one or many emails (mint pending tokens)
    • DELETE /admin/users/<email> — revoke
    • POST /admin/users/<email>/rotate — discard + re-mint
    • GET /admin/mismatches — audit log of email-mismatch incidents

Persistence: SQLite at /data/tokens.db inside the container, backed by the gateway-data Docker volume.

5.3 The OTel collector (server/otel-collector-config.yaml)

Standard otel/opentelemetry-collector-contrib:0.114.0. Receives OTLP from gateway, processes, exports.

Critical processors:

  • memory_limiter — drop oldest batches on memory pressure
  • resource/claude-code — upsert service.name=claude-code (defensive)
  • deltatocumulativethe metrics fix. Claude Code emits sums as DELTA; Prometheus expects CUMULATIVE. Without this, all claude_code_* metrics were silently dropped and VM only had target_info.
  • attributes/cleanup — drops noisy fields (process.pid, etc.)
  • batch — batch before exporting

Exporters:

  • prometheusremotewrite → VictoriaMetrics (http://vmsingle:8428/api/v1/write)
  • otlphttp/vlogs → VictoriaLogs (http://vlogs:9428/insert/opentelemetry)
    • Critical header: VL-Stream-Fields: "service.name,user.email,terminal.type" — without this, VL stores events with empty _stream, making Loki-style selectors return zero matches.

5.4 VictoriaMetrics + VictoriaLogs (storage)

Both FROM scratch images (no shell, no curl) — that's why we removed healthchecks from compose. Their HTTP servers ARE listening, you just can't exec wget inside the container.

  • vmsingle v1.106.1 — metrics. Retention -retentionPeriod=90d.
  • vlogs :latest — logs. Retention -retentionPeriod=90d.

Storage lives in Docker named volumes vm-data and vl-data.

5.5 Grafana (grafana/grafana:11.3.1)

Sub-path served via Apache (GF_SERVER_SERVE_FROM_SUB_PATH=true, GF_SERVER_ROOT_URL=${PUBLIC_BASE_URL}/).

Datasources auto-provisioned via server/grafana-provisioning/datasources.yaml:

  • VictoriaMetrics — type: prometheus, uid: vm
  • VictoriaLogs — type: victoriametrics-logs-datasource (native plugin, installed via GF_INSTALL_PLUGINS), uid: vl

Dashboards auto-provisioned from dashboards/*.json:

  • 01-overview.json — org-wide
  • 02-per-user.json — drill into one employee
  • 03-model-comparison.json — model usage
  • 04-prompt-browser.json — rich filtering (user, project, branch, model, OS, version, terminal)

CSRF env vars (GF_SECURITY_CSRF_TRUSTED_ORIGINS=telemetry.example.com) — necessary behind the path-prefix proxy to prevent "origin not allowed" errors.

5.6 Apache (host, outside Docker)

A snippet of vhost config (committed at server/apache-optel.conf) is included from /etc/apache2/sites-available/000-default-le-ssl.conf via:

## Optel ##
Include /var/www/project/claude-code-optel/server/apache-optel.conf
## Optel ##

Routes inside the snippet:

  • Static file aliases for /install/* (HTML, scripts)
  • ProxyPass /install/ !critical — excludes /install/ from the generic Grafana ProxyPass below it. Without this, mod_proxy claims /install/* before mod_alias has a chance and forwards everything to Grafana.
  • ProxyPass /register → gateway
  • ProxyPass /admin/ → gateway
  • ProxyPass /v1/ → gateway
  • ProxyPass /api/live/ (WebSocket) → Grafana
  • ProxyPass / (catch-all) → Grafana

6. Common operations

6.1 Admin tasks (all via web UI — no SSH)

https://telemetry.example.com/projects/optel/install/admin.html

Task UI action
Add employees Paste emails (one per line) in textarea, click "Add users"
Rotate someone's token (lost laptop, etc.) Find row, click "Rotate"
Revoke (leaver) Find row, click "Revoke"
Audit mismatches Scroll to "Recent email mismatches" section
See active users Top of users table — count of confirmed

CLI equivalent (if web UI is down) — runs on the host:

docker compose exec gateway python /app/mint_token.py mint alice@spaceo.in
docker compose exec gateway python /app/mint_token.py bulk-mint /data/emails.txt
docker compose exec gateway python /app/mint_token.py list
docker compose exec gateway python /app/mint_token.py rotate alice@spaceo.in
docker compose exec gateway python /app/mint_token.py revoke alice@spaceo.in

6.2 Viewing telemetry

https://telemetry.example.com/projects/optel/dashboards → Claude Code folder

Dashboard Use for
Overview Org-wide health: active users, sessions, tokens, cache efficiency, edit acceptance
Per-user Drill-down One employee's full picture (token usage, projects, prompts)
Models & Efficiency Which models used most, cache hit per model, errors
Prompt Browser Free-form filtering: user × project × branch × model × OS × install ver, with live prompt table

For ad-hoc queries → Explore in sidebar → pick VictoriaMetrics (metrics) or VictoriaLogs (logs).

Field-name gotcha: VM uses underscored labels (service_name, user_email, claude_install_version). VL uses dotted labels (service.name, user.email, claude.install_version). It's the same data, just normalized differently by the two backends.

LogsQL syntax (for VL queries):

{service.name="claude-code"} _msg:"claude_code.user_prompt" user.email:"alice@spaceo.in"
  • Stream selector {x=y} (curly braces, exact match)
  • Field filter x:"y" (after a space, with colon, NOT pipe)

6.3 Employee install

https://telemetry.example.com/projects/optel/install/

Enter Claude account email → page returns pre-filled install command (auto-tab to your OS: macOS / Linux / Windows) → paste into terminal → restart Claude Code sessions.

Multi-device install: same flow on the new machine, page detects you're already enrolled and shows a confirmation button before revealing the same token.

6.4 Deploys (Jenkins, no manual steps)

Jenkins watches main branch on GitLab. On every push it runs:

git pull origin main
docker compose build gateway
docker compose up -d --build
docker compose restart otelcol grafana   # ← important: picks up mounted-file changes

Manual deploys needed only when:

  • Apache vhost changes (DevOps needs to copy/include — already set up with Include)
  • .env changes (secrets — never in git)
  • Initial setup on a new server (run server/install.sh once)

7. Repository layout (current)

.
├── README.md                                ← you are here
├── .gitignore
│
├── client/                                  Served by Apache as /install/*
│   ├── index.html                           Install page (email→token flow + OS tabs)
│   ├── admin.html                           Admin web UI (manage users, audit mismatches)
│   ├── install-mac.sh                       Mac/Linux install (Python-based JSON merge)
│   ├── install-linux.sh                     Alias to install-mac.sh
│   ├── install-win.ps1                      Windows PowerShell install
│   ├── uninstall.sh                         Remove OTEL keys from settings.json + .zshrc
│   ├── version.json                         Current install version (read by install page)
│   └── settings.snippet.json                Reference (not actively used)
│
├── gateway/                                 Custom Python service (built into Docker image)
│   ├── app.py                               Flask app: /register, /v1/*, /admin/*
│   ├── mint_token.py                        Admin CLI (mint, rotate, revoke, list, bulk-mint)
│   ├── Dockerfile                           python:3.12-slim + Flask + requests + opentelemetry-proto
│   ├── requirements.txt
│   └── README.md
│
├── server/                                  Deployed to the host at /var/www/project/claude-code-optel/server
│   ├── docker-compose.yml                   Defines gateway, otelcol, vmsingle, vlogs, grafana
│   ├── otel-collector-config.yaml           OTLP receiver + processors + exporters to VL/VM
│   ├── apache-optel.conf                    Vhost snippet (Include'd from main vhost)
│   ├── env.template                         Copy to .env and fill in
│   ├── install.sh                           One-shot first-deploy script
│   ├── jenkins-deploy.sh                    Optional: more polished Jenkins script (current Jenkins uses inline)
│   ├── devops-setup.sh                      One-shot: replaces vhost block + adds sudoers
│   ├── configure-retention.sh               Sets retention TTL on VL/VM (no longer needed; built into flags)
│   └── grafana-provisioning/
│       ├── datasources.yaml                 VM + VL auto-config
│       └── dashboards.yaml                  Where Grafana finds dashboard JSONs
│
├── dashboards/                              Auto-loaded by Grafana on startup
│   ├── 01-overview.json
│   ├── 02-per-user.json
│   ├── 03-model-comparison.json
│   ├── 04-prompt-browser.json
│   └── README.md
│
└── docs/                                    Deeper docs (this README is the canonical entry point)
    ├── EMPLOYEE_SETUP.md                    Employee-facing install guide (also live at /install/)
    ├── EMPLOYEE_CONSENT.md                  Signable consent form (legal review needed!)
    ├── DATA_POLICY.md                       What's collected, retention, who can see it
    └── GRAFANA_SETTINGS.md                  Reference for Grafana env vars + provisioning

8. Lessons learned (the hard-won bits)

These cost real time during development. Documented so they don't bite again.

8.1 docker compose doesn't recreate containers on mounted-file changes

Editing otel-collector-config.yaml, datasources.yaml, dashboard JSON does NOT trigger docker compose up -d to recreate the container. The mounted file changes on disk but the running process doesn't re-read it.

Fix: Jenkins runs docker compose restart otelcol grafana at the end so file changes apply. For tight diagnosis, --force-recreate <service> reads fresh env vars too (which restart does not).

8.2 Apache: mod_proxy beats mod_alias

A generic ProxyPass /projects/optel/ catches everything under that path INCLUDING /install/* — even though we have Alias /projects/optel/install/ mapping to a static directory. mod_proxy runs first in Apache's URL pipeline.

Fix: explicit ProxyPass /projects/optel/install/ ! BEFORE the generic ProxyPass. The ! means "don't proxy this path; let other handlers (Alias) take over."

8.3 ProxyPass inside <Location> — later wins

When ProxyPass is inside <Location> blocks and multiple Locations match, the LATER one wins regardless of specificity. So <Location /projects/optel/v1/> followed by <Location /projects/optel/> would route /v1/* to the latter.

Fix: use vhost-level ProxyPass directives (not inside Location). At that level, first-match-wins as you'd expect. Order specific → generic.

8.4 VictoriaLogs doesn't implement the full Loki query API

/loki/api/v1/push (ingestion) works. But /loki/api/v1/query_range, /labels, /index/stats, /index/volume return "unsupported path requested". Grafana's built-in Loki datasource calls all of these → every log panel was silently empty.

Fix: install the native VictoriaLogs Grafana plugin (victoriametrics-logs-datasource) via GF_INSTALL_PLUGINS. The plugin uses LogsQL at /select/logsql/query which IS supported.

8.5 LogsQL syntax differs from Loki

  • Loki: {x="y"} | name="z" (pipe filter)
  • LogsQL: {x="y"} name:"z" (space + colon)

Dashboard log panels need LogsQL syntax. Loki pipe syntax returns "unexpected pipe" errors.

8.6 VictoriaMetrics silently drops delta-temporality sums

prometheusremotewrite exporter expects CUMULATIVE aggregation. Claude Code's OTel JS SDK emits sums with DELTA temporality. The exporter drops them silently (no error), only target_info makes it through.

Fix: deltatocumulative processor in the metrics pipeline (otelcol-contrib has it). Converts delta sums to running cumulative before export.

8.7 VictoriaLogs ingest needs _stream_fields

Without specifying which OTLP attributes become stream labels, VL stores events with empty _stream={} — Loki stream selectors return zero matches even though the data is right there.

Fix: HTTP header VL-Stream-Fields: service.name,user.email,terminal.type on the otlphttp exporter config. Tells VL to promote those resource attributes to stream labels.

8.8 Field names: dots in VL, underscores in VM

prometheusremotewrite normalizes service.nameservice_name. VictoriaLogs preserves the dot. So queries differ by datasource:

  • VM (PromQL): claude_code_session_count_total{user_email="..."}
  • VL (LogsQL): {service.name="claude-code", user.email="..."}

Don't try to use the same field name across both. Dashboards have to use the right one per panel datasource.

8.9 Grafana CSRF behind sub-path proxy

Grafana 11 rejects POSTs with "origin not allowed" when behind a reverse proxy serving a path-prefixed URL — the browser's Origin header doesn't quite match what Grafana's CSRF check expects.

Fix: GF_SECURITY_CSRF_TRUSTED_ORIGINS=telemetry.example.com + GF_SECURITY_CSRF_ADDITIONAL_HEADERS=X-Forwarded-Host

8.10 Token rotation is the recovery path for any auth weirdness

If an employee sees HTTP 401 from telemetry — even after recent install — most likely cause is settings.json has a stale token (different from DB). Admin runs rotate, user re-fetches via install page. This will be the single most common support ticket. Document it for org rollout.


9. Data model summary

Metrics emitted by Claude Code (visible in VictoriaMetrics)

Metric Labels
claude_code_session_count_total user_email, model, start_type, etc.
claude_code_active_time_seconds_total user_email
claude_code_token_usage_tokens_total user_email, model, type=input/output/cacheRead/cacheCreation
claude_code_cost_usage_USD_total user_email, model
claude_code_lines_of_code_count_total user_email, type=added/removed
claude_code_commit_count_total user_email
claude_code_pull_request_count_total user_email
claude_code_code_edit_tool_decision_total user_email, decision=accept/reject, tool, language

Events emitted by Claude Code (visible in VictoriaLogs)

_msg Notable fields
claude_code.user_prompt prompt, prompt.id, prompt_length
claude_code.api_request model, cost_usd, input_tokens, output_tokens, cache_*_tokens, duration_ms, request_id
claude_code.api_error model, error, status_code
claude_code.tool_decision tool_name, decision, source
claude_code.tool_result tool, success, duration_ms
claude_code.hook_execution_* hook_name, hook_event
claude_code.mcp_server_connection server_scope, status
claude_code.plugin_loaded plugin.name, plugin.version

Common attributes on ALL events (stream labels in VL, normalized labels in VM)

Attribute Source
service.name = "claude-code" Resource attr from Claude Code
user.email From Anthropic account
user.id / user.account_uuid Anthropic account IDs
organization.id Org ID
session.id One per claude invocation
terminal.type "WarpTerminal", "iTerm", "vscode", etc.
host.arch, os.type, os.version Standard OTel resource
service.version Claude Code version
claude.install_version Our custom (from install script)
claude.project_dir / project_name Our custom (from shell wrapper)
claude.git_branch / claude.git_repo Our custom (from shell wrapper, when in git repo)

10. What's NOT emitted (privacy by design)

Thing Why
File paths edited Anthropic doesn't put these in telemetry
Tool inputs (bash commands run, search queries) Privacy
Tool outputs (command stdout, file contents) Privacy
Claude's response text Privacy
Specific code suggested vs accepted Only boolean accept/reject

11. Known limitations

Workaround
Grafana Explore auto-fires a Loki-syntax log-volume query that VL rejects, showing red banner Cosmetic — ignore the banner; manual queries work
One token per email (no per-device tokens) Multi-device with same token is supported; per-device revoke not
No automated backups of gateway-data volume yet Document for DevOps as TODO
No SSO (Grafana login uses local user) Acceptable for admin-only access; could add later if needed
Token in settings.json is cleartext on employee laptops Standard for OTel; acceptable threat model on trusted laptops

12. Open / future work

  • DNS alias (telemetry.example.com → the host) so future server moves don't require employee re-install
  • Automated backups of gateway-data (most critical — losing it means everyone re-installs)
  • Legal/HR review of docs/DATA_POLICY.md + docs/EMPLOYEE_CONSENT.md before org rollout
  • Org rollout comms — send install URL to remaining ~99 employees
  • Optional: SSO/OIDC for Grafana if admin role expands beyond 1–2 people
  • Optional: alerting on high mismatch_count or other anomalies
  • Optional: per-user self-serve dashboard — employees see their own stats

13. Quick reference

Need to Run / Visit
Add an employee Admin UI → "Add employees" textarea
Install on your Mac https://telemetry.example.com/projects/optel/install/
See all prompts a user sent Per-user dashboard or Prompt Browser, filter by user
Check who's on old install version Prompt Browser → Install ver filter, or PromQL: count by (claude_install_version) (claude_code_session_count_total)
Audit suspicious activity Admin UI → bottom section "Recent email mismatches"
Deploy a code change Push to GitLab main → Jenkins picks up automatically
Restart a service DevOps SSH + docker compose restart <service>
Get someone a new token (lost laptop) Admin UI → find user → click "Rotate" → tell them to re-fetch via install page
See live counters docker compose exec gateway python3 -c "import urllib.request as u; print(u.urlopen('http://otelcol:8888/metrics').read().decode())"
Query VL directly docker compose exec gateway python3 -c "import urllib.request as u; print(u.urlopen('http://vlogs:9428/select/logsql/query?query={service.name=\"claude-code\"}&limit=5').read().decode()[:3000])"

14. Contact

Role Contact
Maintainer Space-O Technologies
Issues / questions https://github.com/YOUR-ORG/claude-code-optel/issues
Source https://github.com/YOUR-ORG/claude-code-optel

About

Space-O Claude Optel

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages