为 Claude 订阅构建可观测性仪表盘:claude-observability 解析 Claude Code 会话文件
Building an Observability Dashboard for a Claude Subscription
开发者 FirePing32 开源 claude-observability,用本地采集器 claude-obs 读取 Claude Code 会话文件,将用量上传至 Next.js 仪表盘,实现账户级分析。采集器约 2,000 行 TypeScript 且无运行时依赖,应用约 6,500 行,采用推送模型,仅上传数字、不触碰登录凭证,支持多机汇总与 5 小时限额追踪。
目录
Claude 订阅的用量到底去了哪里?
Claude Max 订阅给你很多 Claude Code 用量。它不会告诉你
你是怎么用的。/usage 显示当前窗口的百分比
以及何时重置。这就是全部报告:没有历史,没有会话或模型,
也没有跨机器的信息。
API 客户能获得带有用量和成本报告的 Admin API。订阅者 则没有类似的东西。与此同时,本可以回答这个问题的数据就摆在 明处,在 Claude Code 的会话文件里,散布在你使用的每一台笔记本、 台式机和远程开发机上。
所以我构建了 claude-observability:
一个在每台机器上运行的小型收集器 claude-obs,以及一个 Next.js 仪表盘,把收集器
上传的内容转化为账户级分析。收集器大约 2,000 行 TypeScript(无运行时依赖),
应用约 6,500 行,外加一个两者都依赖的小型共享包。
有意思的部分不是图表。而是:
- 从本不该被读取的文件中得出可信的数字
- 把提示词和代码留在机器上
- 在你无法控制的电脑上运行后台进程
快速开始
- 源码: github.com/FirePing32/claude-observability,MIT 许可
- 收集器: npm 上的
claude-obs,Node 20+
自行部署仪表盘(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 上要花多少钱?
- 有没有东西缺失? 有机器停止上报了吗?我们是否触及了 已跟踪用量无法解释的限制?
正是这些约束让它成为一个项目:
- 没有可拉取的 API。唯一的来源是每台机器上的本地文件,其格式 Anthropic 并未公开文档,且会随意更改。
- 账户才是单位。限制适用于账户,而非某台机器。只显示一台笔记本用量的仪表盘回答的是错误的问题。
- 敏感信息不离开机器。会话文件保存着每一条提示、每一条回复、Claude 读取的每一个文件。采集器必须只上传数字,别的什么都不传,而且这一点必须可证明,而非仅靠承诺。
- 不涉及凭据。采集器绝不能触碰 Claude 登录令牌。
答案长什么样

这是我账户过去 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 个。
系统的形态
这是一个推送模型。每台机器运行 claude-obs,它读取 Claude Code 自上次运行以来追加到其会话文件中的字节。它将这些字节归约为用量记录并上传。服务器将来自每台机器的上传合并到一个账户中,为每个请求定价,并提供仪表盘。
一个工作区就是一个 Claude 账户。机器注册到工作区中,显示的一切都是账户级别的。这里刻意没有按人查看的视图:目标是账户的正确总量,而不是排行榜。

机器是仪表盘唯一提供的细分维度,因为采集器静默或配置错误是最可能导致总量出错的原因。对于每台机器(此处名称已匿名化),它显示:
- 其价值占比
- 其模型构成和 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。任何未知内容只被计数,不被解析。仪表盘在数据质量下显示这些计数,因此格式变化会表现为数字上升,而不是静默地产生错误的总计。
一个响应,多行
第一个真正的意外来自在编写任何解析器逻辑之前先进行计数。
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 分钟进行一次完整重新扫描,作为遗漏事件的安全网。
先写入磁盘,再触碰网络
操作顺序就是整个可靠性故事:
- 将新行解析为批次。
- 通过临时文件和重命名,将每个批次写入一个发件箱目录,每批一个文件。
- 以同样的方式将新偏移量提交到
state.json。 - 按顺序上传发件箱,仅在服务器接受后才删除每个文件。
在第 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 自己的核算(最后一次运行),而不是作为通过/失败检查。

上面的会话说明了原因。它跨多次恢复运行了近六天: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:utilparseArgs,内置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 点重置”,而不是学习到的估算值。
五件值得知道的事
- 先计数再解析。 未记录的格式会因重复而说谎。最大的正确性 bug 在一行计数中就可见。
- 让每个操作幂等,然后就不用担心了。 一个 upsert 键让崩溃、重新同步、两台机器和两个数据源都变成同一件无关紧要的事。
- 先写磁盘再走网络,并规划排空队列。 持久化会像保存你的数据一样忠实地保存你的错误。
- 无法测试的隐私只是一个声明。 在测试夹具中放一个金丝雀字符串,比任何程度的谨慎都更便宜。
- 移除功能可以成为安全修复。 邀请和分享链接是绕过允许列表的两种方式。删除它们比任何检查都更有效。
代码在 GitHub 上:FirePing32/claude-observability。MIT 许可。采集器在 npm 上名为 claude-obs。
它与 Anthropic 无关联。“Claude”和“Claude Code”是 Anthropic 的商标。如果你发现上述数字或推理有误之处,请提交 issue。
2026-10-12
来源:Hacker News · prakhargurunani.com