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.comand the repo placeholderYOUR-ORG/claude-code-optel. Replace both with your own host and repo. The deployment URL is set per-install via theOTEL_BASE_URL/PUBLIC_BASE_URLenvironment variables (seeserver/env.template).
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.
- 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 to127.0.0.1only — the proxy is the sole public entrypoint. A ready-to-include Apache config is atserver/apache-optel.conf(theserver/devops-setup.shscript wires it into an existing vhost).opensslfor generating the token.
# 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 -dThen point your reverse proxy at the stack (see server/apache-optel.conf) so
https://<your-host>/projects/optel/ reaches Grafana and the gateway.
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 |
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.comEmployees then self-install from https://<your-host>/projects/optel/install/.
Security note:
ADMIN_TOKENgrants full user-management access andGRAFANA_ADMIN_PASSWORDis your dashboard root login — keep.envout of git (it's already in.gitignore) and treat both as secrets.
| 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.
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 |
| 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 |
┌───────────────────────────────────────────────────────────────────────────┐
│ │
│ 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 │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────┘
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.
The single most important custom piece. Lives at 127.0.0.1:18888 inside Docker.
Three responsibilities:
-
POST /register— install-page email→token lookup- First call (status=pending): returns token immediately
- First call (status=confirmed): returns
requires_confirmation: trueWITHOUT token - Second call with
confirm_reinstall: true: returns token (multi-device flow) - Revoked: returns 410. Email not on list: returns 404.
-
POST /v1/{logs,metrics}— OTLP proxy with auth + identity binding- Validates
Authorization: Beareragainst tokens table - Parses incoming OTLP/protobuf payload to extract
user.emailattribute - Compares to email the token was issued for — rejects with 403 if mismatch
- Logs every mismatch to
mismatchestable for admin audit - Marks token
confirmedon first valid match - Forwards valid traffic to
otelcol:4318
- Validates
-
GET/POST/DELETE /admin/*— admin operations (requireADMIN_TOKEN)GET /admin/users— list everyone with statePOST /admin/users— add one or many emails (mint pending tokens)DELETE /admin/users/<email>— revokePOST /admin/users/<email>/rotate— discard + re-mintGET /admin/mismatches— audit log of email-mismatch incidents
Persistence: SQLite at /data/tokens.db inside the container, backed by the
gateway-data Docker volume.
Standard otel/opentelemetry-collector-contrib:0.114.0. Receives OTLP from
gateway, processes, exports.
Critical processors:
memory_limiter— drop oldest batches on memory pressureresource/claude-code— upsertservice.name=claude-code(defensive)deltatocumulative— the metrics fix. Claude Code emits sums as DELTA; Prometheus expects CUMULATIVE. Without this, allclaude_code_*metrics were silently dropped and VM only hadtarget_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.
- Critical header:
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.
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 viaGF_INSTALL_PLUGINS), uid:vl
Dashboards auto-provisioned from dashboards/*.json:
01-overview.json— org-wide02-per-user.json— drill into one employee03-model-comparison.json— model usage04-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.
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→ gatewayProxyPass /admin/→ gatewayProxyPass /v1/→ gatewayProxyPass /api/live/(WebSocket) → GrafanaProxyPass /(catch-all) → Grafana
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.inhttps://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)
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.
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 changesManual deploys needed only when:
- Apache vhost changes (DevOps needs to copy/include — already set up with Include)
.envchanges (secrets — never in git)- Initial setup on a new server (run
server/install.shonce)
.
├── 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
These cost real time during development. Documented so they don't bite again.
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).
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."
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.
/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.
- 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.
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.
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.
prometheusremotewrite normalizes service.name → service_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.
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
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.
| 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 |
_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 |
| 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) |
| 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 |
| 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 |
- 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.mdbefore 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
| 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])" |
| 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 |