ByteNoteByteNote
把 trace 送出去:Exporter 的活儿
字

字节笔记本

2026年10月24日 · 约 13 分钟读完

把 trace 送出去:Exporter 的活儿

API中转
¥120

模型配置:模型选择与上下文窗口

本文是《从 0 写 Agent》系列第 27 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Exporter:把 trace 送出去」。

BatchProcessor 收集了 trace/span,但它们要去哪?控制台?文件?HTTP 端点?OpenTelemetry collector?

Exporter 决定 trace 的"目的地"。agents-go 内置五种 exporter,从最简单的 no-op 到生产级的 HTTP 导出。这一章拆它们,重点是 HTTPExporter 的一个铁律,"telemetry 绝不能反过来搞挂应用"。


引子:trace 要送到哪

不同场景,trace 的目的地不同:

场景目的地
本地开发调试控制台(直接打印)
测试内存收集(断言用)
生产监控HTTP 端点(发到 Jaeger/Datadog/自建)
不需要 traceno-op(零开销)
自定义函数(任意逻辑)

agents-go 内置五种 exporter 覆盖这些场景。


成本管理:token 用量与预算

Exporter 接口

极其简单(tracing/tracing.go:83):

go
// tracing/tracing.go:83
type Exporter interface {
    Export(items []any)  // items 是 *Trace 和 *Span 的混合
}

一个方法,接收一个 []any(Trace 和 Span 混合)。 实现方用 type-switch 区分:

go
func (e *MyExporter) Export(items []any) {
    for _, item := range items {
        switch v := item.(type) {
        case *tracing.Trace:
            // 处理 trace
        case *tracing.Span:
            // 处理 span
        }
    }
}

为什么是 []any 而不是分开 []*Trace 和 []*Span? 因为 BatchProcessor 是批量导出的,一个批次里 trace 和 span 混着(trace start + 若干 span)。一个 []any 携带整个批次,exporter 自己 type-switch 分拣。比"调两次 Export(一次 trace 一次 span)"高效。


五种内置 Exporter

1. NoopExporter,什么都不做

go
type NoopExporter struct{}
func (NoopExporter) Export([]any) {}

默认 exporter。 不配任何 exporter 时,trace 被收集但不导出(零开销丢弃)。适合"我不需要 trace"的场景。

2. FuncExporter,函数适配

go
type FuncExporter func(items []any)
func (f FuncExporter) Export(items []any) { f(items) }

把一个普通函数变成 exporter。 适合快速自定义,不用定义新类型:

go
exporter := tracing.FuncExporter(func(items []any) {
    for _, item := range items {
        if span, ok := item.(*tracing.Span); ok {
            log.Printf("span: %s %v", span.Name, span.StartedAt)
        }
    }
})

3. ConsoleExporter,打印到控制台

go
type ConsoleExporter struct {
    W io.Writer
    mu sync.Mutex  // 并发安全
}

JSON 行格式打印每个 item。 带 mutex(多 goroutine 并发写安全)。本地调试最常用。

4. CollectingExporter,内存收集(测试用)

go
type CollectingExporter struct {
    Items []any
}

把所有 item 收集到内存切片。 主要给测试用,跑完 Run 后断言"产生了哪些 span"。

go
ce := &tracing.CollectingExporter{}
// ... 跑 Agent(用 ce 做 exporter)...
// 断言
assert.Equal(t, 5, len(ce.Items))  // 产生了 5 个 span

5. HTTPExporter,发到 HTTP 端点

go
type HTTPExporter struct {
    Endpoint string        // 目标 URL
    Client   *http.Client
    // ...
}

生产级 exporter,把 trace 批量 POST 到 HTTP 端点。 可以接到 Jaeger、Datadog、自建的 trace 服务。


HTTPExporter 的铁律:失败静默丢弃

HTTPExporter 是最容易出问题的,网络可能挂、端点可能 500。agents-go 的处理(tracing/http_exporter.go:54):

go
// 概念示意
func (e *HTTPExporter) Export(items []any) {
    data, _ := json.Marshal(items)
    resp, err := e.Client.Post(e.Endpoint, "application/json", bytes.NewReader(data))
    if err != nil {
        atomic.AddInt64(&e.failedCount, 1)  // 计数
        return  // 静默丢弃,不重试
    }
    io.Copy(io.Discard, resp.Body)  // drain body 保 keep-alive
    resp.Body.Close()
}

失败时,计数,然后丢弃,不重试。 这是铁律:telemetry 绝不能反过来搞挂应用。

为什么?

  • 不重试,重试会累积。如果端点持续失败,重试的 trace 越积越多,最终 OOM
  • 不 panic,trace 系统挂了,Agent 不该跟着挂
  • 计数,让你知道"丢了多少",可观测

注释明说(http_exporter.go:17),HTTPExporter 刻意不兼容 OpenAI 专有 dashboard schema(那是 OpenAI 内部格式)。它是通用的 JSON 导出,接你自己的后端。

drain body 保 keep-alive

go
io.Copy(io.Discard, resp.Body)

这个小细节很重要,读完响应 body 再关闭,让 HTTP 连接能复用(keep-alive)。如果不 drain,连接不能复用,每次导出都新建 TCP 连接,开销大。


设计权衡:Exporter 失败该不该通知

有人会想,exporter 失败了,是不是应该告警?让用户知道 trace 丢了?

agents-go 选择"计数但不告警",failedCount 字段在那,你可以查"丢了多少",但 exporter 不会主动发告警。

为什么? 因为 exporter 自己不能发告警,它可能就是发到告警系统的(循环依赖)。告警应该由监控 failedCount 指标的外部系统做(Prometheus 等),不是 exporter 自己。

这是关注点分离,exporter 只管"导出 + 计数",告警归监控系统。


怎么用 Exporter

完整的使用示例:

go
// 1. 创建 exporter
exporter := tracing.NewConsoleExporter(os.Stdout)

// 2. 创建 BatchProcessor(把 exporter 接上)
proc := tracing.NewBatchProcessor(exporter, tracing.BatchProcessorOptions{
    MaxBatchSize:  64,
    MaxQueueSize:  1024,
    FlushInterval: 5 * time.Second,
})
defer proc.Shutdown(context.Background())  // 关闭时导出剩余

// 3. 创建 Tracer
tracer := tracing.NewTracer(proc)

// 4. Agent Run 时传入 tracer
agents.Run(ctx, agent, input, agents.RunOptions{
    Tracer:        tracer,
    ModelProvider: p,
})

// Run 产生的 trace/span → BatchProcessor → ConsoleExporter → 打印

四步,exporter(目的地)→ processor(异步批处理)→ tracer(创建 span)→ Run 传入。 清晰的分层。


Part IV 回顾

第 21-27 章,让 Agent 健壮且可观测:

章节能力
21 Router多 provider 路由
22 Retry容忍临时故障
23 Fallback容忍彻底故障
24 组合装饰器任意嵌套
25 Tracing 架构四层可观测系统
26 BatchProcessor异步流水线
27 Exportertrace 导出

这七章让 Agent 从"能跑"变成"健壮、可监控、出错能定位"。 但 Agent 还连接不了外部世界,MCP 工具、Skills、Sandbox。这是 Part V 的内容。


下一篇

第 28 章,进入 Part V,MCP 协议:连接外部工具的统一标准。Agent 不能只用手写的 Go 函数工具,要能连接 MCP server 提供的工具生态。agents-go 的 mcp 包是怎么适配 MCP 协议的?schema 兼容、多模态回传、错误回喂,每个都有坑。


本文是《从 0 写 Agent》系列第 27 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: