TraceLink 最早不是一个准备发布的开源项目,而是业务工程里的一组调试代码。
当时的问题很具体:一个请求从浏览器进入后端,再经过数据库、第三方服务或后台任务,普通日志能告诉我每个局部发生了什么,却很难说明这些局部怎样连成了一次完整运行。日志越加越多,排查时仍然需要靠时间戳和直觉把它们拼起来。
我想要的不是更多文本,而是一条可以还原的调用路径:它从哪里开始,当前函数是谁调用的,在哪个分支被阻断,跨过一次 await 后父子关系是否还在,以及整个过程耗时多久。
这组代码后来经历了两次抽离。第一次抽出了目录和包,第二次才真正抽出了产品边界。
起点:日志缺少的不是数量,而是上下文
一条孤立日志通常只回答“这里发生了什么”。调试一条复杂链路时,我还需要回答:
- 这条记录属于哪一次完整调用(trace)?
- 当前调用节点(span)的父节点是谁?
- 它属于哪个可开关的调试范围(Scope)?
- 这是一次正常调用、主动阻断,还是仅表达调用意图?
- 一个 span 何时开始、何时结束、持续多久?
- JavaScript 与 Python 产生的数据能否放进同一条链路?
于是业务工程里逐渐出现了 tracer、middleware、NDJSON 文件和一个简单的读取界面。它并不漂亮,但验证了最重要的假设:面对跨前端、后端与异步任务的问题时,一条保留父子关系的运行时链路,通常比一批互不关联的日志更容易使用。
这时我做了第一次抽离。
第一次抽离:仓库独立了,边界仍然属于原业务
第一次做法很自然:把调试代码搬进独立仓库,再按照当时对系统的理解拆出 core、插件、服务端和 Python 包。目录看起来已经像一个正式项目,也有示例和构建脚本。
真正接入另一个工程后,问题才集中出现。
自动注入逻辑只有进入下游项目才能验证;生成后的代码藏在构建结果里,问题很难定位;不同项目的目录、运行时和前后端组合并不相同;使用者需要先理解不少内部实现,才能判断自己是否接对了。
那一轮工作的主要成果是“代码离开了业务仓库”,但下面这些问题仍然没有稳定答案:
- 谁负责生成标准追踪数据?
- 谁负责传输,谁负责接收和落盘?
- Receiver 不可用时,业务是否仍然正常运行?
- JavaScript 和 Python 复用的是代码,还是协议?
- 用户到底需要安装几个包、启动几个进程?
- 一个全新项目如何证明安装包真的可用?
我一度把它概括为“抽包失败”。后来再看,问题并不在“抽包”这件事本身,而在于过早决定了包和目录,数据契约与职责边界却还没有冻结。
第一次抽出了代码,却没有抽出稳定边界。
第二次抽离:先确定数据怎样流动
重新设计时,我不再从“应该拆几个包”开始,而是先画清数据流:
应用埋点
-> Language SDK
-> Engine:span 生命周期、ID、上下文、Scope 门控
-> Runtime:浏览器、Node、Python 各自的上下文实现
-> Transport:Exporter 与跨服务传播
-> TraceLink Protocol
-> Receiver:接收、落盘、SSE 推送、Scope 控制
-> Dashboard / 导出脚本 / 其他调试工具这张图带来了几个比目录更重要的决定。
跨语言共享协议,不假装共享一个 Core
JavaScript 和 Python 都会生成 TraceLog,字段语义由同一份 Schema 与 golden fixtures 约束。但它们维护异步上下文的方式不同:Node 使用 AsyncLocalStorage,Python 使用 ContextVar,浏览器也有自己的运行限制。
因此,TraceLink 没有创造一个能被所有语言共同导入执行的“跨语言 Core”。真正复用的是 Protocol、Receiver 和 Dashboard;每种语言用符合自身运行时习惯的方式实现 Engine 与上下文传播。
这个区别看似只是措辞,实际决定了新语言应该怎样接入:先实现符合协议的小型 Exporter;如果需要自动维护 span 父子关系,再增加该语言自己的上下文 Engine。
看板中的跨运行时链路,以同一个 Receiver 为汇合点
Exporter 可以直接写入本地文件,也可以把事件发送给 Receiver。Receiver 负责 NDJSON 持久化、SSE 增量推送和 Scope 策略;当需要在 Dashboard 中联合查看前端、后端或多个进程的调用时,它们必须指向同一个 Receiver。
这也划清了“导出追踪事件”和“传播业务请求上下文”的区别:前者把观测数据交给 Receiver,后者把 traceId 与 parentSpanId 写进真正的业务请求。两个动作都需要,但不能混为一谈。
追踪失败不能改变业务结果
TraceLink 被设计成开发期辅助工具,而不是业务依赖。自定义 Exporter、控制流或 Dashboard 出错时,不应该让被调试的应用跟着失败。
这条约束影响了发送、缓存和错误处理的实现,也成为产品边界的一部分:追踪可以缺失,但不能篡改应用原本的行为。
逻辑有四层,不等于发布四个 npm 包
Engine、Runtime、Transport、Receiver/Dashboard 需要在架构上分清,但用户不应该为这种清晰承担多包安装和版本协调成本。
TraceLink 的 JavaScript 交付设计收敛为一个 npm 包,通过 subpath exports 暴露不同运行入口:
tracelink
tracelink/browser
tracelink/node
tracelink/receiver/http
tracelink/receiver/vitePython SDK 则设计为独立的 PyPI 发行包,因为 npm 与 PyPI 本来就是两个发行生态。Dashboard 构建为单文件并嵌入 Receiver,目标是让使用者通过一个入口启动本地接收端和看板,而不需要再协调一组界面包。
这不是为了追求“包越少越高级”,而是在逻辑职责与安装成本之间做取舍。
发布前最后一道边界:验证真正的安装包
源码目录里能运行,仍然不能证明用户拿到的包能运行。
TraceLink 的发布验证会构建 JavaScript 和 Python 产物,检查协议 fixtures、类型与测试,再通过 npm pack 生成真实 tarball。相邻的 Dashboard 工程安装这个实体 tarball,而不是依赖源码目录联接,然后重新构建并嵌入最终看板。
这样做同时解决了两个问题:它更接近用户从 registry 安装的路径,也避免 Windows 目录联接在清理依赖时误伤源码。
Dashboard 还使用 TraceLink 追踪自己的交互和内部计算。一次操作可以经过 SDK、Exporter、Receiver,再回到 Dashboard 被显示出来。这种自举不是为了展示技巧,而是让完整链路在发布前持续接受真实使用。
现在的 TraceLink 是什么
截至当前仓库版本 v0.6.6,TraceLink 包含:
- JavaScript 与 Python SDK,以及共享的协议和验证样例;
- 本地 Receiver、NDJSON 与可读日志落盘;
- Scope 控制与 SSE 实时同步;
- 日志表、实时调用图、调用骨架图、时间线和 SVG 导出;
- 一个可选的 Skill,帮助 AI 编程助手添加埋点或分析生成后的调试数据。
它仍然只面向本地开发期,不是生产可观测性后端,也不会自动理解所有业务语义。显式接入虽然多一步,却让埋点可搜索、可关闭、可测试,也更容易由开发者和代码助手共同维护。AI 不是 TraceLink 特殊追踪的运行对象,只是调试数据的可选分析者之一。
这两次抽离留下的判断
如果把整个过程压缩成一句话,我会写:
可复用工具的第一资产不是目录结构,而是稳定契约;产品化也不是把源码搬出去,而是让陌生人能够安装、理解、验证和安全地失败。
这条判断不只属于追踪工具。只要一个内部脚本准备走向外部用户,就会遇到同样的问题:哪些东西应该成为公开契约,哪些只是当前实现,谁拥有状态,出错时由谁承担后果。
TraceLink 的两次抽离,只是让我把这个问题看得更清楚。
试用与反馈
项目源码与文档已经公开在 GitHub,JavaScript 与 Python SDK 也已发布到 npm 和 PyPI:
npm install tracelink
pip install tracelink如果你正在调试跨前后端请求、异步任务、后台任务或跨服务调用,可以先查看架构、协议和示例;发现接入边界不清楚或实现问题时,欢迎直接提交 Issue。理解成本和失败方式上的反馈,比“功能看起来很多”更能帮助它继续变好。