写作归档

Workers下的Tracing实现

类型
文章
日期
2026/10/16
阅读
6 分钟
字数
1,487

1、实现目标

上一篇介绍了可观测性的几个基本概念:Trace 是一次请求的完整快照,Span 记录每个节点的执行过程,degraded 用来标记没有报错、但执行效果已经变差的情况。我们也比较了 LangSmith、Langfuse 和自建 Tracing 三种方案各自适合的场景。

这一篇解决一个具体实现问题:如何在 Cloudflare Workers 环境中落地一套可用的 Tracing 系统。

开始设计之前,需要先了解 Cloudflare Workers 的运行环境。Workers 是边缘计算平台,代码会部署在全球数百个节点上,请求由距离用户较近的节点处理。这种模式降低了访问延迟,同时也带来了几项约束

约束说明
不能依赖实例常驻请求结束后 Worker 实例可能被回收,因此不能把日志长期积攒在内存中,再依赖下一次请求集中写入
CPU 预算有限CPU 时间有套餐和配置上限,应避免在请求中执行不必要的大量计算;网络等待时间与 CPU 时间是两个不同的概念
提供配套存储Cloudflare 提供 D1(关系型数据库)和 KV(键值存储)等服务,可以直接在 Worker 中访问

基于这些约束,这套 Tracing 实现需要满足四个目标:

  • 每次请求生成唯一的 traceId,用来把这次请求的所有信息串起来
  • 每个管线节点都可以通过统一包装记录输入、输出、耗时和状态,减少开发者手动记录日志的工作
  • 优先保留异常请求,正常请求按比例采样,控制存储成本
  • 写入过程不阻塞用户响应,Tracing 不能拖慢业务

承担这些职责的核心对象是 TraceContext,它会贯穿一次请求的整个生命周期。

2、TraceContext 设计

一次请求包含多个 Span,每个 Span 对应一个管线节点,例如安全检查、记忆检索或 LLM 生成。TraceContext 负责在请求执行期间收集和管理这些 Span。

它并不只是保存几条自由文本日志,而是把每个节点的执行过程转换成结构化的 Span。每当一个节点执行结束,TraceContext 都会追加一条记录,其中包含节点名称、耗时、输入、输出和状态。

我们先定义 Span 需要记录的字段。这些字段与上一篇介绍的 Span 结构保持一致:

span.ts

interface Span {
  spanId: string            // 当前节点的唯一标识
  parentSpanId: string | null // 父节点 ID,用于构建树形结构
  name: string              // 节点名称,如 'memory_retrieval'
  startTime: number         // 开始执行的时间戳
  duration: number          // 执行耗时(毫秒)
  status: 'ok' | 'error' | 'degraded'  // 本文定义的业务状态
  input: Record<string, unknown>   // 节点接收的输入
  output: Record<string, unknown>  // 节点产生的输出
  metadata: Record<string, unknown> // 额外的诊断信息
}

parentSpanId 用于表示 Span 之间的嵌套关系。例如,memory_retrieval 内部包含 vector_search 和 structured_query 两个子操作,它们可以通过 parentSpanId 指向父 Span。读取 Trace 时,便能据此还原完整的调用层级。

这里的 degraded 是业务层状态,不是 OpenTelemetry 的标准 Span 状态。若要和 OpenTelemetry 对接,应映射到 UNSET、OK、ERROR 等标准状态,再把 degraded 作为业务属性或事件记录。input、output 也不应默认保存原始用户消息,生产环境应先做脱敏、截断或哈希处理。

定义好 Span 后,就可以实现 TraceContext。它提供一个 span() 方法,用来包装任意异步操作,并自动完成计时、状态判断和结果记录。

trace.ts

class TraceContext {
  readonly traceId: string
  private spans: Span[] = []
  private startTime: number

  constructor(traceId?: string) {
    // 如果外部传入了 traceId 就用外部的,否则自动生成一个
    // 这在跨服务追踪时很有用:上游服务可以把 traceId 传下来
    this.traceId = traceId ?? crypto.randomUUID()
    this.startTime = Date.now()
  }

  // 核心方法:包装一个异步操作,自动记录为 Span
  // <T> 是 TypeScript 的泛型——意思是"返回类型取决于你传入的函数返回什么"
  // 比如 fn 返回 Promise<string>,那 span() 的返回值也是 Promise<string>
  async span<T>(
    name: string,
    input: Record<string, unknown>,
    fn: () => Promise<T>,
    parentSpanId?: string
  ): Promise<T> {
    const spanId = crypto.randomUUID()
    const start = Date.now()
    let status: Span['status'] = 'ok'
    let output: Record<string, unknown> = {}

    try {
      const result = await fn()
      output = { result }
      return result
    } catch (err) {
      status = 'error'
      output = { error: (err as Error).message }
      throw err  // 记录完错误后继续抛出,不吞掉异常
    } finally {
      // 无论成功还是失败,都会执行 finally 块
      // 在当前执行没有被终止的前提下,确保这个 Span 会被记录下来
      this.spans.push({
        spanId,
        parentSpanId: parentSpanId ?? null,
        name,
        startTime: start,
        duration: Date.now() - start,
        status,
        input,
        output,
        metadata: {}
      })
    }
  }

  // 手动将某个已记录的 Span 标记为降级状态
  // 适用于"执行成功了,但结果不理想"的场景
  // 示例按顺序执行且节点名称唯一;并发或重复节点应改用 spanId 定位
  markDegraded(spanName: string, reason: string) {
    const span = this.spans.find(s => s.name === spanName)
    if (span) {
      span.status = 'degraded'
      span.metadata.degradeReason = reason
    }
  }

  // 判断是否有任何 Span 出错
  hasError(): boolean {
    return this.spans.some(s => s.status === 'error')
  }

  // 判断是否有任何 Span 降级
  hasDegraded(): boolean {
    return this.spans.some(s => s.status === 'degraded')
  }

  // 获取从创建到现在的总耗时
  get totalDuration(): number {
    return Date.now() - this.startTime
  }

  // 将整个 Trace 序列化为 JSON,方便存储和传输
  toJSON() {
    return {
      traceId: this.traceId,
      totalDuration: this.totalDuration,
      spanCount: this.spans.length,
      spans: this.spans
    }
  }
}

这个抽象能够降低节点接入 Tracing 的成本。开发者不必在每个节点中重复编写开始计时、结束计时、异常捕获和结构化输出等逻辑,只需要用 trace.span() 包装原来的函数调用。

下面把 TraceContext 接入一条简单的 AI 管线。用户发送消息后,系统会依次执行安全检查、记忆检索和 LLM 生成:

usage.ts

// 1. 在请求入口创建 TraceContext 实例
const trace = n

订阅后解锁剩余内容

已免费试看前 30%,订阅小册即可阅读全文。

订阅小册
0 条

还没有评论。

发表评论