
字节笔记本
2026年10月24日 · 约 13 分钟读完
把 trace 送出去:Exporter 的活儿

本文是《从 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/自建) |
| 不需要 trace | no-op(零开销) |
| 自定义 | 函数(任意逻辑) |
agents-go 内置五种 exporter 覆盖这些场景。

Exporter 接口
极其简单(tracing/tracing.go:83):
// tracing/tracing.go:83
type Exporter interface {
Export(items []any) // items 是 *Trace 和 *Span 的混合
}一个方法,接收一个 []any(Trace 和 Span 混合)。 实现方用 type-switch 区分:
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,什么都不做
type NoopExporter struct{}
func (NoopExporter) Export([]any) {}默认 exporter。 不配任何 exporter 时,trace 被收集但不导出(零开销丢弃)。适合"我不需要 trace"的场景。
2. FuncExporter,函数适配
type FuncExporter func(items []any)
func (f FuncExporter) Export(items []any) { f(items) }把一个普通函数变成 exporter。 适合快速自定义,不用定义新类型:
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,打印到控制台
type ConsoleExporter struct {
W io.Writer
mu sync.Mutex // 并发安全
}JSON 行格式打印每个 item。 带 mutex(多 goroutine 并发写安全)。本地调试最常用。
4. CollectingExporter,内存收集(测试用)
type CollectingExporter struct {
Items []any
}把所有 item 收集到内存切片。 主要给测试用,跑完 Run 后断言"产生了哪些 span"。
ce := &tracing.CollectingExporter{}
// ... 跑 Agent(用 ce 做 exporter)...
// 断言
assert.Equal(t, 5, len(ce.Items)) // 产生了 5 个 span5. HTTPExporter,发到 HTTP 端点
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):
// 概念示意
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
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
完整的使用示例:
// 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 Exporter | trace 导出 |
这七章让 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 移植版)。全系列目录见总览。

