跳到正文
Hacker News· prakhargurunani·· 6 天前AI 评分34

为 Claude 订阅构建可观测性仪表盘:claude-observability 解析 Claude Code 会话文件

Building an Observability Dashboard for a Claude Subscription

AI 导读

开发者 FirePing32 开源 claude-observability,用本地采集器 claude-obs 读取 Claude Code 会话文件,将用量上传至 Next.js 仪表盘,实现账户级分析。采集器约 2,000 行 TypeScript 且无运行时依赖,应用约 6,500 行,采用推送模型,仅上传数字、不触碰登录凭证,支持多机汇总与 5 小时限额追踪。

正文 · AI 翻译

目录

  1. 快速开始
  2. The problem
    1. 答案长什么样
  3. 系统的形态
  4. The collector
    1. 会话文件没有文档,所以解析白名单
    2. 一次响应,多行
    3. 读尾部字节,而不是整个文件
    4. 先写磁盘,再碰网络
    5. 隐私是测试,不是承诺
    6. 企业网络会击穿天真的 HTTPS
    7. 后台代理
  5. The server
    1. 来自两个来源的幂等摄取
    2. 把机器绑定到单个账户
    3. 谁可以上传
    4. 谁可以查看
    5. 重建 5 小时区块
    6. 价值,而非成本
    7. 洞察与告警
  6. How it was built
    1. 先规划,并写下决策
    2. 写解析器之前先度量数据
    3. 用独立来源验证
    4. 让上线过程发现 bug
    5. 技术栈
  7. 它看不到什么
  8. 五件值得知道的事

Claude 订阅的用量到底去了哪里?

Claude Max 订阅给你很多 Claude Code 用量。它不会告诉你 你是怎么用的。/usage 显示当前窗口的百分比 以及何时重置。这就是全部报告:没有历史,没有会话或模型, 也没有跨机器的信息。

API 客户能获得带有用量和成本报告的 Admin API。订阅者 则没有类似的东西。与此同时,本可以回答这个问题的数据就摆在 明处,在 Claude Code 的会话文件里,散布在你使用的每一台笔记本、 台式机和远程开发机上。

所以我构建了 claude-observability: 一个在每台机器上运行的小型收集器 claude-obs,以及一个 Next.js 仪表盘,把收集器 上传的内容转化为账户级分析。收集器大约 2,000 行 TypeScript(无运行时依赖), 应用约 6,500 行,外加一个两者都依赖的小型共享包。

有意思的部分不是图表。而是:

  • 从本不该被读取的文件中得出可信的数字
  • 把提示词和代码留在机器上
  • 在你无法控制的电脑上运行后台进程

快速开始

自行部署仪表盘(README 中有完整步骤),登录,为你的 Claude 账户 创建一个工作区,并生成一个注册码。然后,在每一台使用该账户运行 Claude Code 的机器上:

npm install -g claude-obs
claude-obs login --code XXXX-XXXX --server https://<your-dashboard>
claude-obs sync            # uploads your full history the first time
claude-obs install-agent   # keeps it up to date in the background

想在注册前确切看到离开机器的内容? claude-obs sync --dry-run 会打印出来,不发送任何东西。


问题

这些问题问起来很简单:

  • 我是不是快要触及 5 小时限制了? 昨天我离它有多近?
  • 是什么在消耗它? 哪些会话、哪些项目、哪些模型?Opus 是不是 在悄悄做 Sonnet 就能做的工作?
  • 这个套餐值吗? 同样的 token 在按量付费 API 上要花多少钱?
  • 有没有东西缺失? 有机器停止上报了吗?我们是否触及了 已跟踪用量无法解释的限制?

正是这些约束让它成为一个项目:

  1. 没有可拉取的 API。唯一的来源是每台机器上的本地文件,其格式 Anthropic 并未公开文档,且会随意更改。
  2. 账户才是单位。限制适用于账户,而非某台机器。只显示一台笔记本用量的仪表盘回答的是错误的问题。
  3. 敏感信息不离开机器。会话文件保存着每一条提示、每一条回复、Claude 读取的每一个文件。采集器必须只上传数字,别的什么都不传,而且这一点必须可证明,而非仅靠承诺。
  4. 不涉及凭据。采集器绝不能触碰 Claude 登录令牌。

答案长什么样

The overview page

这是我账户过去 30 天的概览。上面的每个问题对应一个卡片:

  • 5 小时限制。当前区块已使用价值 $40.67,还剩两小时。这是本账户学习到的限制(约 $165,来自三次真实的限制触发)的 25%。按当前每小时 $16.80 的消耗速度,到重置时将接近 45%。
  • 这个套餐值吗?本计费周期内,$100 的套餐产生了价值 $2,289 的 API 等效用量:22.9 倍。续费后第二天就回本了。
  • 是什么在消耗?Opus 占价值的 98%。每日图表还显示了 9 月底从 Opus 5 到 Opus 5.5 的切换,无需任何人去记住它何时发生。
  • 有遗漏吗?五台机器都在上报。但遥测覆盖率为 0%,因此总量偏低几个百分点,而且 claude.ai 聊天完全未被计量。页面如实说明,而非隐瞒。

在卡片下方,第一条洞察正是启动这个项目的原因:本周在 2 个区块中触发了用量限制,上周为 1 个。


系统的形态

Architecture

这是一个推送模型。每台机器运行 claude-obs,它读取 Claude Code 自上次运行以来追加到其会话文件中的字节。它将这些字节归约为用量记录并上传。服务器将来自每台机器的上传合并到一个账户中,为每个请求定价,并提供仪表盘。

一个工作区就是一个 Claude 账户。机器注册到工作区中,显示的一切都是账户级别的。这里刻意没有按人查看的视图:目标是账户的正确总量,而不是排行榜。

The machines page, with machine names anonymized

机器是仪表盘唯一提供的细分维度,因为采集器静默或配置错误是最可能导致总量出错的原因。对于每台机器(此处名称已匿名化),它显示:

  • 其价值占比
  • 其模型构成和 Opus 占比
  • 其最后活跃和最后上传的时间

由两台机器上报的同一请求只计一次,归入第一台机器。

该仓库是一个 npm 工作区,包含三个包:

  • packages/shared:传输模式(zod)和价格表。采集器用它发送时校验的同一模式来校验发送内容,服务器也用它校验接收内容,因此两者不会产生偏差。
  • packages/cli:采集器,用 tsup 打包成单个文件。
  • apps/web:Next.js 16 应用。它包含摄取端点、仪表盘、告警和数据库迁移。

采集器

会话文件没有文档,因此只解析允许列表

Claude Code 每行写入一个 JSON 对象到 ~/.claude/projects/<project>/<session>.jsonl,另外每个会话还有一个 subagents/ 文件夹用于子代理运行。有几十种记录类型:助手消息、标题、压缩标记、轮次时长、Claude Code 自己的成本快照、权限模式变更等等。新类型会在版本之间出现。

解析器根据 type 进行分发,只处理它能理解的记录。它从显式路径逐字段构建每一条输出记录:不做对象展开,不复制 message。任何未知内容只被计数,不被解析。仪表盘在数据质量下显示这些计数,因此格式变化会表现为数字上升,而不是静默地产生错误的总计。

一个响应,多行

第一个真正的意外来自在编写任何解析器逻辑之前先进行计数。

Collector pipeline

Claude Code 为响应的每个内容块写一行,并在每一行上重复该响应的 usage 对象。一个包含文本块和工具调用的回复是两行,且 token 计数相同。把它们相加就会重复计数。在我自己的历史记录中,3,301 条 assistant 行实际上对应 1,889 个不同的请求。朴素求和多计了约 75%。

这比单纯的重复更微妙。对于其中 39 个请求,output_tokens 在各行之间不同,且最后一行总是最大的。这些是流式更新。所以规则是:按 requestId 分组,并取最大值,绝不求和。收集器仅当请求的输出增长时才重新发送该请求,这能在响应仍在流式传输时保持实时会话的准确性。

服务器应用同样的规则,因此乱序的批次不会降低某个值:

// never let a concurrent, staler write lower the final streamed output
output: sql`greatest(${schema.apiRequests.output}, excluded.output)`,

其他记录需要各自处理。带有 model: "<synthetic>" 的行根本不是请求。它们是 Claude Code 写入转录的 API 错误和使用限制通知。收集器在本地将它们分类为一个类别(prompt_too_long、rate_limit、五小时或每周限制等),并且只上传类别,绝不上传文本。

尾部字节,而非文件

会话文件会增长到数十兆字节。每次变更都重新读取它们会很浪费,因此收集器为每个文件保留一个字节偏移量,以及其设备号和 inode 号:

  • 它从偏移量读取到 EOF,并且只消费完整的行。末尾没有换行符的行仍在写入中,因此它会等待下一轮。
  • 它独立地从字节解码每一行,因此跨读取块拆分的多字节字符绝不会被破坏。
  • 如果 inode 发生变化或文件缩小,说明文件已被替换,因此它从零开始读取。这是安全的,因为服务器会去重。

在监视模式下,它使用带防抖的递归 fs.watch,并每 15 分钟进行一次完整重新扫描,作为遗漏事件的安全网。

先写入磁盘,再触碰网络

操作顺序就是整个可靠性故事:

  1. 将新行解析为批次。
  2. 通过临时文件和重命名,将每个批次写入一个发件箱目录,每批一个文件。
  3. 以同样的方式将新偏移量提交到 state.json。
  4. 按顺序上传发件箱,仅在服务器接受后才删除每个文件。

在第 2 步和第 3 步之间崩溃意味着这些行会被再次读取并排队两次。服务器只存储每个请求一次,所以这无害。在其他任何地方崩溃都会将该批次留在发件箱中供下次运行处理。一台离线一周的笔记本电脑只会积累一个队列。每一次失败都是重发,绝不是丢失。

上传失败会被分类:

  • 网络错误、5xx 和 429 会退避并重试,遵守 Retry-After。
  • 401(令牌被撤销) 会停止代理并要求重新登录。
  • 422(模式被拒绝) 会将那一个批次连同原因移到隔离文件夹,这样单个坏记录就不会永远阻塞队列。

隐私是测试,不是承诺

“我们只上传使用数据”很容易写在 README 里。仓库通过三种方式强制执行:

  • 逐字段构建器,如上所述。没有任何代码路径会把对象从转录记录复制到输出中。
  • 金丝雀测试。测试夹具会在消息内容、电子邮件地址和文件路径中植入标记字符串。如果其中任何一个出现在序列化上传内容的任何位置,测试就会失败。
  • claude-obs sync --dry-run 会精确打印将要发送的内容,因此任何人都可以在注册前自行检查。

实际上传的内容包括:模型、时间戳、token 计数(五分钟和一小时缓存写入分开列出)、effort、入口点、子代理类型、项目文件夹的 basename(或其加盐哈希)、git 分支以及会话标题(可以关闭)。

企业网络会破坏朴素的 HTTPS

我自己机器之外的第一台机器立刻就失败了:SELF_SIGNED_CERT_IN_CHAIN。Netskope 和 Zscaler 这类安全代理会拦截 HTTPS,并用公司根证书重新签名。操作系统信任该根证书。Node 默认不信任:它自带 CA 证书包。

让每个用户都设置 NODE_EXTRA_CA_CERTS 会带来支持负担。因此收集器像浏览器一样,自动信任操作系统的证书存储:

  • 较新的 Node(22.19+、24.5+):tls.setDefaultCACertificates 会在进程内合并系统证书。
  • macOS 上的较旧 Node:它会用 /usr/bin/security 导出 System 钥匙串和 Apple 的根证书,将证书包缓存一周,然后用指向它的 NODE_EXTRA_CA_CERTS 重新启动自身一次。Node 只在启动时读取该变量,因此需要重新执行。
  • Linux 上的较旧 Node:它会以同样的方式指向发行版的 CA 证书包。

还有一个陷阱:后台服务不会继承你 shell 的环境。在终端中可用的代理和证书变量在 launchd 下根本不存在。代理安装程序会将它们复制到服务定义中。

后台代理

claude-obs install-agent 在 macOS 上作为 launchd 代理、在 Linux 上作为 systemd 用户单元、在 Windows 上作为任务计划程序作业以低优先级运行 sync --watch。

一个 PID 锁文件将其限制为单个实例。该锁在退出时以及收到 SIGINT、SIGTERM 和 SIGHUP 时释放,死进程留下的锁会被检测并替换。在代理运行时手动运行 claude-obs sync 只会轻推正在运行的实例。


服务器

来自两个来源的幂等摄取

会话文件并非全部真相。Claude Code 还会发出一些小的附带请求,例如生成会话标题和运行分类器,这些请求永远不会进入转录记录。这占使用量的 5–10%。

不过,Claude Code 可以导出 OpenTelemetry 事件,其 api_request 事件包含这些附带请求和延迟。因此该应用也有一个 OTLP/HTTP 接收器,并且 claude-obs otel --install 会将正确的设置合并到 Claude Code 的配置中,并保留备份。

两个来源描述的是相同的请求,因此一切都是以工作区和请求 ID 为键的 upsert:

  • 转录记录的 token 计数优先,因为它们带有五分钟和一小时缓存的拆分,而 OTel 将其合并为一个数字。
  • 仅存在于 OTel 的字段(如持续时间)在后续转录记录上传后仍会保留。
  • output 只会增长。

每一行都记录哪些来源看到过它。这就是 数据质量 所显示的:两者都看到、仅转录记录看到,或仅遥测看到。

将机器固定到一个账户

一个工作区只能包含一个 Claude 账户的使用量。如果有人注册了一台登录到不同账户的机器,所有总计都会在无声无息中出错。

采集器从 Claude Code 的本地配置中读取账户 UUID,并发送 HMAC-SHA256(salt, accountUuid),而从不发送 UUID 本身。盐是 按工作区分配的,在注册时下发,因此该账户下的每台机器 都会生成相同的哈希。如果盐是按机器分配的,“这是同一个账户吗?” 将无法回答。首次上传会将工作区固定到该哈希, 来自任何其他账户的上传都会被以 409 拒绝。

谁可以上传

机器通过工作区所有者生成的短注册码进行注册。注册码以哈希形式存储,会过期,并有使用次数限制。还有 一种设备码流程:CLI 打印一个代码,所有者在浏览器中 看到机器的名称、操作系统和 IP 后批准它。无论哪种方式,机器 都会获得一个随机的 256 位令牌。服务器只存储其 SHA-256,并且每台 机器都可以单独撤销。

在此之上还有一道电子邮件检查。工作区可以要求机器 使用已批准的电子邮件地址登录 Claude。采集器从不 发送该地址。它发送 sha256("claude-obs-email-v1:" + email), 服务器将其与所有者的地址进行比较,然后丢弃它。

这不是密码学证明,因为机器报告的是自己的电子邮件。它 能阻止错误和随意滥用。注册码和令牌才是真正的 锁。

谁可以查看

登录是通过 Better Auth 进行的 Google OAuth,受电子邮件允许列表限制。 该检查在两个数据库钩子中运行:创建账户时以及每次 会话开始时。移除某人的操作会在其下次登录时生效,而不是在 之后的某个清理时刻。每个页面、查询和服务器操作都会检查工作区 成员资格。

项目中最有用的安全变更是移除。第一个 版本有邀请功能,因此成员可以添加某人,还有用于 只读仪表板的公开分享链接。两者都很方便。两者都意味着能 看到数据的人是由任何已登录的人决定的,而不是由允许列表决定的。 我移除了它们。现在允许列表是唯一的门。

重建 5 小时区块

Anthropic 将订阅限制描述为从你发送第一条消息开始的五小时会话。它没有公布确切的规则,因此仪表板 模拟它们:

if (!cur || e.ts >= cur.end) {
  const start = Math.floor(e.ts / HOUR) * HOUR;  // round down to the hour
  cur = { start, end: start + BLOCK_MS, /* … */ };
}

一个区块从上一个区块过期后的第一个请求开始,持续 五小时。在活动区块内,仪表板显示已用价值、每小时 消耗速率以及对重置的预测。

它无法知道的是限制的大小。因此它学习一个。每当 账户触及五小时限制时,该区块中追踪到触及时的价值就是一个样本。这些样本的中位数就是该账户学习到的上限, 而第 10 百分位数是一个保守值。

同样的数据能捕捉到一些有用的东西:如果在追踪到的使用量 远低于保守上限时触及了限制,那么采集器看不到的某个东西 正在使用该账户。通常是 claude.ai 聊天。仪表板将其标记为 未见的用量,而不是假装数字是完整的。

价值,而非成本

在订阅中,没有任何东西按令牌计费。因此每个美元数字都是 API 等效价值:相同的令牌在按量付费 API 上 本应花费的金额。它是根据一个明确的价格表计算的:输入、输出、 缓存读取、五分钟和一小时缓存写入、网络搜索,以及 快速模式和仅限美国推理的乘数。

缓存价格按模型列出,而不是由比率推导得出,因为各模型之间的比率不同。未知模型会被存储并计为未定价,而不是猜测。将周期价值除以套餐价格,你就能得到大多数人真正想要的数字:套餐回本了多少倍。

洞察与提醒

洞察是规则,而不是模型:

  • 限额压力
  • 缓存命中率下降
  • 失控会话
  • Opus 密集周
  • 静默机器
  • 未见的用量

可选的每周摘要仅将聚合数据发送给 Claude 以生成书面总结。不会发送任何会话或标题。

提醒会发送到 Slack、webhook 或电子邮件,按规则和按区块去重。我的托管套餐只允许每日 cron 任务,这会让“你已达到区块的 90%”变得毫无用处。因此提醒也会在每次上传后进行评估。真实活动会触发检查,而每日任务只需捕捉活动的缺失:一台陷入静默的机器。


它是如何构建的

先做计划,并写下决策

这个项目始于一份计划文档,而不是代码。塑造它的那些问题在任何代码编写之前就已得到回答:

  • 推送还是拉取?推送,因为没有什么可拉取的。
  • 按人还是按账户?按账户,因为限额是按账户计算的。
  • 什么算作真相?每个请求 id 一行,从所有来源合并而来。

这三个答案决定了大部分模式设计。

在编写解析器之前先度量数据

75% 的重复计数并不是一个假设。它来自在解析器存在之前,对一份真实的 44 MB 历史记录中行数与唯一请求 id 进行计数对比。同一轮检查还发现了流式增长、子代理文件夹和合成错误行。否则,其中每一项都会变成一份 bug 报告。

用独立的东西进行验证

收集器的总计数据与一个从头编写的独立脚本进行了核对。两者相差不超过一个请求:$235.18 对 $235.27,这个请求是在两者同时运行时发出的。

显而易见的第二项检查结果比预期要弱。Claude Code 会记录每个会话自己的成本摘要,但仅针对最后一次进程运行。恢复会话会重置它,而且它包含转录记录永远看不到的侧边请求。单次运行的会话精确到分匹配。恢复的会话则不然,原因很充分。因此仪表盘将该数字显示为Claude Code 自己的核算(最后一次运行),而不是作为通过/失败检查。

A session’s detail page

上面的会话说明了原因。它跨多次恢复运行了近六天:6,683 个请求和 $698 的跟踪价值。Claude Code 自己的记录显示为 $309,因为它只覆盖最后一次运行。

同一页面还显示了两件你无法从 Claude Code 内部看到的事情:

  • 每个请求的上下文大小。它攀升超过 800K token,直到一次压缩将其降回约 100K,发生了两次。
  • 子代理。它们在 6,683 个请求中占了 5,742 个,并贡献了 76% 的价值。

让上线过程去发现 bug

第一批真实机器发现了两个任何测试都没有发现的问题:

  • 上述企业 TLS 问题。0.1.3 版本增加了更清晰的网络错误和自动系统证书信任。
  • 队列卡在旧版本上。 0.1.4 版本将电子邮件证明设为强制要求。已升级的机器上仍有 0.1.3 排队的批次没有该证明,服务器对每个批次都永远回复 426,请升级。持久化发件箱忠实地保留了永远无法被接受的批次。0.1.5 版本在发送时升级排队的批次。它会从机器当前登录信息中填充证明,但仅当批次的账户哈希与该登录信息匹配时。

这个教训可以推广:持久化队列也是你旧传输格式的持久化副本。 要为排空它做好规划。

技术栈

  • 采集器: Node 20+ 上的 TypeScript,用 tsup 打包成单个文件。node:util parseArgs,内置 fetch,zod 已打包。无运行时依赖、无原生模块,也不对 CLI 本身进行遥测。
  • 应用: Next.js 16(App Router、server actions、React 19),使用 Google 的 Better Auth,基于 postgres.js 的 Drizzle ORM,Tailwind CSS 4,Recharts 和 Vitest。
  • 数据: 托管 Postgres。原始事实保留 13 个月,摄取日志保留 90 天。

它看不到什么

明确说明这一点比任何图表都重要:

  • claude.ai 聊天、桌面应用的聊天和云端 Claude Code 会话 使用相同的限额,但不留下本地文件。未使用标记是它们唯一的证据。
  • 历史记录有保质期。 Claude Code 默认在 30 天后删除会话文件(cleanupPeriodDays)。在较晚注册的机器上,任何早于此的内容都已消失。
  • 没有 OpenTelemetry 时,总计会偏低 5–10%,因为存在旁路请求。
  • 五小时区块是一个模型。 它们符合已记录的行为,而非已发布的规范。

自然的下一步能弥补最大的缺口。Claude Code 将账户官方的五小时和每周利用率传递给状态栏脚本,以百分比和重置时间表示。那是 Anthropic 自己的计量器,覆盖包括聊天在内的所有界面。采集器可以在不接触凭据的情况下记录它,仪表盘随后可以显示“剩余 62%,下午 3 点重置”,而不是学习到的估算值。


五件值得知道的事

  1. 先计数再解析。 未记录的格式会因重复而说谎。最大的正确性 bug 在一行计数中就可见。
  2. 让每个操作幂等,然后就不用担心了。 一个 upsert 键让崩溃、重新同步、两台机器和两个数据源都变成同一件无关紧要的事。
  3. 先写磁盘再走网络,并规划排空队列。 持久化会像保存你的数据一样忠实地保存你的错误。
  4. 无法测试的隐私只是一个声明。 在测试夹具中放一个金丝雀字符串,比任何程度的谨慎都更便宜。
  5. 移除功能可以成为安全修复。 邀请和分享链接是绕过允许列表的两种方式。删除它们比任何检查都更有效。

代码在 GitHub 上:FirePing32/claude-observability。MIT 许可。采集器在 npm 上名为 claude-obs。

它与 Anthropic 无关联。“Claude”和“Claude Code”是 Anthropic 的商标。如果你发现上述数字或推理有误之处,请提交 issue。

2026-10-12 

来源:Hacker News · prakhargurunani.com