cc-otel

module
v0.1.0-preview.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2026 License: MIT

README

English Documentation

Claude Plugin Go Version License Release Coverage Go Report Card Test Platform Downloads

CC-OTEL

Claude Code Token 用量监控服务。接收 OTEL 遥测数据,提供 Web 仪表盘查看 token 消耗和费用。

文档

Dark Dashboard Dark

Light Dashboard Light

为什么需要

Claude Code 内置了 OpenTelemetry 支持,但查看数据需要搭建 Grafana、Prometheus 或使用第三方 SaaS。CC-OTEL 是一个单二进制文件,接收 OTLP 遥测数据,存储到 SQLite,提供 Web 仪表盘 -- 无外部依赖,零配置即可运行。

架构

Claude Code ──OTLP gRPC(:4317)──> cc-otel ──> SQLite
                                      |
                                  Web UI <── Browser (localhost:8899)

功能

  • OTLP gRPC 接收器 -- 接收 Claude Code 的指标和日志事件
  • Web 仪表盘 -- Token 用量、费用明细、缓存命中率、按模型统计
  • KPI 分项 -- 点击任意 KPI 卡片查看模型级别明细
  • 实时更新 -- SSE 推送,新数据到达时自动刷新
  • 深色/浅色主题 -- 自动跟随系统偏好
  • 日期范围 -- Today、7 Days、30 Days、All Time,或自定义日期
  • 图表切换 -- Tokens、Cost、Requests 视图
  • 会话追踪 -- 按会话聚合费用和 Token
  • 预聚合表 -- 查询延迟 < 3ms,百万行无压力
  • 单二进制 -- go:embed 打包 Web UI,零运行时依赖
  • 跨平台 -- Windows、macOS、Linux

安装

Claude Code 插件(推荐)
/plugin marketplace add young1lin/claude-token-monitor
/plugin install cc-otel@claude-token-monitor
/reload-plugins
/cc-otel:setup
可用命令
命令 说明
/cc-otel:setup 下载二进制、配置 OTEL 环境变量、启动服务
/cc-otel:start 启动后台守护进程
/cc-otel:stop 停止守护进程
/cc-otel:status 查看服务状态 + 今日费用摘要
/cc-otel:open 在浏览器中打开 Web 仪表盘
/cc-otel:report [today|7d|30d|all] 生成费用报告
从源码编译
# Linux / macOS
go build -o cc-otel ./cmd/cc-otel/

# Windows
go build -o cc-otel.exe ./cmd/cc-otel/
从 Release 下载

前往 Releases 下载对应平台的二进制文件。

安装到 ~/.claude/cc-otel/
cc-otel install    # 复制二进制到 ~/.claude/cc-otel/(全平台通用)
cc-otel init       # 生成默认配置文件

运行

cc-otel start      # 后台启动
cc-otel status     # 查看状态(版本、PID、端口、今日统计)
cc-otel stop       # 停止
cc-otel serve      # 前台运行(调试用)
cc-otel -v         # 输出版本号
cc-otel cleanup    # 按 retention_days 清理旧数据

打开仪表盘: http://localhost:8899/

原理

什么是 OpenTelemetry?

OpenTelemetry(OTEL)是 CNCF 的可观测性标准,统一了三类遥测信号:

  • Metrics -- 时序指标(token 数、费用、请求数等)
  • Logs / Events -- 结构化事件(每次 API 请求、用户 prompt、工具调用结果)
  • Traces -- 分布式追踪(Claude Code 0.2.x+ 的 beta 功能)

Claude Code 内置 OTEL SDK,通过 OTLP(OpenTelemetry Protocol,标准传输协议)把上述信号导出到任意兼容的后端。CC-OTEL 就是一个专门针对 Claude Code 定制的轻量 OTLP 后端。

数据流
┌─────────────────┐    OTLP/gRPC     ┌──────────────────────┐    ┌────────────────────────┐
│  Claude Code    │ ───────────────▶ │  cc-otel (:4317)     │───▶│  SQLite                │
│  Codex CLI      │    :4317         │  · LogsService       │    │  · api_requests        │
│  (OTEL SDK)     │                  │  · MetricsService    │    │  · codex_api_requests  │
│  logs + traces  │                  │  · TraceService      │    │  · daily_model_agg     │
└─────────────────┘                  └──────────┬───────────┘    └──────────┬─────────────┘
                                                │ Notify()                  │
                                                ▼                           │
                                     ┌──────────────────────┐               │
                                     │  Web UI (:8899)      │◀──────────────┘
                                     │  · REST API          │   query
                                     │  · SSE /api/events   │───┐
                                     └──────────────────────┘   │ push
                                                                ▼
                                                         ┌────────────┐
                                                         │  Browser   │
                                                         └────────────┘
三个阶段

1. 接收(internal/receiver/

内嵌一个 gRPC 服务器,实现 OTLP 官方定义的 LogsServiceMetricsServiceTraceService

  • 每个 claude_code.api_request 日志事件携带一次 API 调用的完整信息(model、tokens、cost、duration、session.id、user.id 等 resource + record attributes),被落库到 api_requests 表。
  • claude_code.token.usageclaude_code.cost.usage 等 Metrics 接收时被显式跳过持久化(与 api_request log 数据冗余),仅用于不让客户端报错。
  • Trace span 用于抽取 ttft_ms 并回填到 api_requests / codex_api_requests 的对应列。
  • raw_otlp_events / codex_raw_otlp_events 表保留 schema 兼容已有的 backfill 工具,但不再写入新数据

2. 存储(internal/db/

SQLite(WAL 模式 + busy_timeout)单文件数据库:

  • api_requests / codex_api_requests -- 每次 API 调用一行,是最小粒度的原始事实表
  • daily_model_agg / codex_daily_model_agg -- 按(日期 × 模型)在写入路径上同步刷新的预聚合表,Web UI 的图表和 Daily Detail 都查这两张,查询延迟 < 3 ms
  • raw_otlp_events / codex_raw_otlp_events -- 原始事件回溯表(已停写)+ 后台扫除:raw_ttl_days(默认 5 天)按小时清理;其余明细表归 retention_days(默认 90 天)管。

Token 统计严格按照 Anthropic 的 Prompt caching 官方口径:输入侧总和 = input_tokens + cache_read_tokens + cache_creation_tokens,三者在 UI 上以「Uncached / Cache Read / Cache Create」三列分项展示。

3. 展示(internal/api/ + internal/web/

  • REST API:/api/dashboard/api/daily/api/sessions/api/status
  • SSE:/api/events — 接收器每次成功入库新数据后调用 Broker.Notify(),通过 Server-Sent Events 推送到浏览器,前端自动刷新图表
  • 静态资源:默认通过 go:embed 打进二进制;本地开发可用 CC_OTEL_STATIC_DIR 从磁盘读取,免重编译
为什么用 gRPC 而不是 HTTP?

Claude Code 支持 grpc / http/json / http/protobuf 三种 OTLP 传输。CC-OTEL 只实现 gRPC,因为:

  • Claude Code 自身对 gRPC 路径做了最多的优化和测试
  • protobuf 编码比 JSON 小 ~40%,在高频写 / 长 session 场景下更省
  • 连接复用(HTTP/2 多路复用)减少导出延迟

如果你的网络环境只能走 HTTP,可在 Claude Code 和 cc-otel 之间架一层 otel-collector 做协议转换。

配置 Claude Code

Claude Code 需要通过 gRPC 导出 OTLP 数据到 CC-OTEL。在 ~/.claude/settings.json"env" 中添加以下环境变量:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "no_proxy": "localhost,127.0.0.1"
  }
}

注意: 只添加/更新以上 OTEL 相关的 key,不要覆盖已有配置。端口号应与 cc-otel.yaml 中的 otel_port 一致。

⚠️ 代理用户必看: 如果你设置了 http_proxy / https_proxy(如 Clash、V2Ray 等代理工具),必须同时设置 no_proxy 排除 localhost,否则遥测数据完全收不到。OTEL gRPC SDK 会把发往 localhost:4317 的流量也走代理,代理无法处理 gRPC 导致连接静默失败,cc-otel 收不到任何数据。设置 "no_proxy": "localhost,127.0.0.1" 即可让 OTEL exporter 直连本地。/cc-otel:setup 会自动添加此项。

Codex CLI 接入

cc-otel 也支持接收 OpenAI Codex CLI 的 OTEL 遥测数据。Codex 与 Claude Code 共用 OTLP gRPC 端口 (:4317),cc-otel 通过 OTLP Resource 的 service.name 字段自动识别来源并路由到独立的 codex_* 表。

~/.codex/config.toml 中加入(备份后追加,不要覆盖已有配置):

[otel]
environment = "dev"
exporter.otlp-grpc.endpoint = "http://localhost:4317"
trace-exporter.otlp-grpc.endpoint = "http://localhost:4317"
metrics-exporter.otlp-grpc.endpoint = "http://localhost:4317"

启动 Codex,正常使用即可。打开 dashboard (http://localhost:8899/?source=codex) 查看 Codex 用量数据。Codex 不上报 cost_usd——cc-otel 会用本地价目表按 token 数自动算出费用并写入 codex_api_requests.cost_usd,Cost KPI 与 Claude 视图等同。

价格表与非 Claude 模型重算

cc-otel 内嵌了从 BerriAI/litellm 派生的价格快照(GPT / GLM / DeepSeek / Kimi / Qwen / Gemini …),首次启动会写入 model_pricing 表。运行时按以下顺序选价:

  1. cc-otel.yamlpricing:(用户覆盖,最高优先级)
  2. SQLite model_pricing(长期存储 + 跨重启)
  3. 每天一次的远端拉取(LiteLLM + OpenRouter,仅 diff 写入)

写入逻辑只有一条规则:

  • modelclaude- 开头(大小写不敏感)→ 信任 Claude Code 上报的 cost_usd,不动。
  • 其他全部模型 → 按本地价目表用 token 数重算并覆盖。

派生效果:Codex (gpt-5-codex 等)、GLM/DeepSeek/Kimi 走 Anthropic 兼容反代时上报的 cost_usd 都会被纠正。

调试:访问 GET /api/pricing/lookup?model=glm-4.6 查看命中策略与价格;右上角 live 弹窗有 Pricing Table 行(绿/黄/红代表上次刷新时间窗)。

历史回填:

# dry-run
go run ./tools/recompute_cost --db ~/.claude/cc-otel/cc-otel.db --table both
# 确认 diff 合理后再 --apply
go run ./tools/recompute_cost --db ~/.claude/cc-otel/cc-otel.db --table both --apply

要禁用每日远端刷新,在 cc-otel.yaml 加:

pricing_refresh:
  enabled: false

配置文件

CC-OTEL 的数据目录按以下顺序解析(详见 internal/config/config.godefaultDataDir):

  1. ./bin/ -- 可执行文件位于名为 bin 的目录中时(开发模式)。
  2. ~/.claude/cc-otel/ -- 否则使用此目录(不存在会自动 mkdir)。
  3. . -- 兜底,仅在无法获取 home 目录时落到当前目录。

注意:~/.claude/ 本身作为中间查找步骤;老版本文档曾如此描述,但代码里没这一步。

所有文件(二进制、配置、数据库、PID、日志)在同一目录:

~/.claude/cc-otel/
├── cc-otel(.exe)    # 可执行文件
├── cc-otel.yaml     # 配置
├── cc-otel.db       # SQLite 数据库
├── cc-otel.pid      # PID 文件
└── cc-otel.log      # 日志

环境变量覆盖(最高优先级):

变量 说明 默认值
CC_OTEL_OTEL_PORT OTLP gRPC 接收端口 4317
CC_OTEL_WEB_PORT Web UI 端口 8899
CC_OTEL_DB_PATH SQLite 数据库路径 ~/.claude/cc-otel/cc-otel.db
数据保留

cc-otel 有两套自动清理:

retention_days: 90    # 全量明细表(api_requests / events / *_events / 各 _agg 等)的留存上限,单位天;0 = 永不清理
raw_ttl_days: 5       # raw_otlp_events / codex_raw_otlp_events 的留存上限(已停写,仅清理历史),单位天;0 = 永不清理
  • retention_days(默认 90)由 cc-otel cleanup 手动跑或后台周期任务使用。
  • raw_ttl_days(默认 5)由后台 hourly sweeper 单独管 raw 表,因为这两张表的体积权重最大。

Web UI

Web UI

状态指示器

右上角绿色点 + live 表示 SSE 推送连接正常。点击打开 Server Status 面板,查看数据库健康状态、OTLP 接收器状态和端点信息。

KPI 分项

点击任意 KPI 卡片(Cost、Input、Output、Cache Hit、Requests)查看按模型的分项数据。

更新

更新插件(命令和技能)
/plugin update cc-otel@claude-token-monitor
更新二进制

/cc-otel:setup 会检查已安装版本并自动更新到最新。

或手动:

# 查看当前版本
~/.claude/cc-otel/cc-otel -v

# 强制重新安装
/cc-otel:setup --force

开发

make build       # 编译(注入版本号)
make test        # 运行所有测试
make coverage    # 生成覆盖率报告
make vet         # go vet 检查

前端开发免重编译:

CC_OTEL_STATIC_DIR=internal/web/static cc-otel serve

License

MIT


duration_ms 是什么?从哪来的?(源码佐证)

CC-OTEL 的 duration_ms 不是由 cc-otel 计算,而是 Claude Code 通过 OTEL log attributes 上报,cc-otel 仅接收并落库后用于 Web UI 展示与聚合统计。

1) CC-OTEL 侧:接收并落库(不做二次计算)

cc-otel 的 OTLP Logs 接收器会把任意 log record 的 attributes 解析为 Event,并直接读取 duration_ms/ttft_ms

// internal/receiver/receiver.go
DurationMs: parseAttrInt(attrs, "duration_ms"),
TTFTMs:     parseAttrInt(attrs, "ttft_ms"),

之后 Event 会被转换为 APIRequest 并写入 SQLite(api_requests.duration_ms),用于 Request Log / Sessions / Dashboard 等查询。

2) Claude Code 侧:duration_ms 的计算口径(源码)

在 Claude Code(source map 还原源码)中:

  • startIncludingRetries = Date.now():整次请求链路开始(包含所有 retries)
  • start = Date.now():每次 attempt(重试的单次请求)开始都会重置
  • durationMs = Date.now() - start本次成功 attempt 的端到端墙钟耗时
  • durationMsIncludingRetries = Date.now() - startIncludingRetries包含 retries 的总墙钟耗时

关键源码片段(计算 start + 重试重置):

// restored-src/src/services/api/claude.ts
const startIncludingRetries = Date.now()
let start = Date.now()
// ...
async (anthropic, attempt, context) => {
  // ...
  start = Date.now() // 每次 attempt 开始重置
  attemptStartTimes.push(start)
  // ... dispatch streaming request ...
}

关键源码片段(把 duration_ms 写入 OTEL api_request 事件 attributes):

// restored-src/src/services/api/logging.ts
const durationMs = Date.now() - start
const durationMsIncludingRetries = Date.now() - startIncludingRetries

void logOTelEvent('api_request', {
  model,
  input_tokens: String(usage.input_tokens),
  output_tokens: String(usage.output_tokens),
  cache_read_tokens: String(usage.cache_read_input_tokens),
  cache_creation_tokens: String(usage.cache_creation_input_tokens),
  cost_usd: String(costUSD),
  duration_ms: String(durationMs),
  speed: fastMode ? 'fast' : 'normal',
})
3) 解释:它不是“思考耗时”

因此,CC-OTEL 展示的 duration_ms 表示 一次 Claude Code API 请求的端到端墙钟耗时(至少包含网络/排队/流式传输/本次 attempt 内的处理)。它 不是“模型纯思考时间”。

备注:Claude Code 代码里也会计算 ttftMs(首 token 时间),但某些版本/事件可能不会把它作为 OTEL attribute 上报,导致 CC-OTEL 中 ttft_ms 常为 0。

Directories

Path Synopsis
cmd
cc-otel command
integration-test
debug-otel command
internal
api
db
logrotate
Package logrotate provides a size-capped log writer.
Package logrotate provides a size-capped log writer.
pricing
Package pricing maintains a per-model USD-per-token price table used to recompute cost_usd for non-Claude models.
Package pricing maintains a per-model USD-per-token price table used to recompute cost_usd for non-Claude models.
web
tools
dump_pricing_snapshot command
dump_pricing_snapshot fetches the BerriAI/litellm model_prices JSON from GitHub raw, filters to providers cc-otel users actually see, and writes a trimmed snapshot to internal/pricing/embed/seed.json.
dump_pricing_snapshot fetches the BerriAI/litellm model_prices JSON from GitHub raw, filters to providers cc-otel users actually see, and writes a trimmed snapshot to internal/pricing/embed/seed.json.
otlp_dump command
Temporary OTLP gRPC dump tool — listens on a port, prints all received LogRecords as JSON.
Temporary OTLP gRPC dump tool — listens on a port, prints all received LogRecords as JSON.
prune_before command
prune_before deletes all rows from cc-otel.db whose timestamp is strictly before the given local-date cutoff.
prune_before deletes all rows from cc-otel.db whose timestamp is strictly before the given local-date cutoff.
recompute_cost command
recompute_cost backfills cost_usd for non-Claude rows in api_requests and / or codex_api_requests using the local pricing registry.
recompute_cost backfills cost_usd for non-Claude rows in api_requests and / or codex_api_requests using the local pricing registry.
snapshot_db command
Online SQLite snapshot via VACUUM INTO.
Online SQLite snapshot via VACUUM INTO.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL