第2讲:分布式追踪(Trace)——让请求在全链路中可见

一、为什么需要分布式追踪?

上一讲我们实现了结构化日志,但日志有一个天然缺陷:它们是一个个孤立的点

想象一个典型的 Agent 请求链路:

复制代码
用户请求 → MCP Gateway → LLM 调用 → 工具调用 A → 工具调用 B → LLM 再次调用 → 返回结果

如果每个步骤各自打日志,你看到的是:

复制代码
[14:23:01] 收到用户请求
[14:23:02] LLM 调用开始
[14:23:03] 工具 A 被调用
[14:23:04] 工具 B 被调用
[14:23:05] LLM 再次调用
[14:23:06] 响应返回

问题来了:

  • 这些日志属于同一个请求吗?------ 没有关联 ID 无法确认
  • 哪一步最慢?------ 只知道每个步骤的耗时,但不知道它们之间的因果关系
  • 如果失败了,是 LLM 的问题还是工具的问题?------ 需要手动拼凑

**分布式追踪(Distributed Tracing)**​ 解决的就是这个问题:通过 Trace → Span → Span 的树形结构,把一次请求的所有步骤串联起来。


二、核心概念

概念 说明 类比
Trace 一次请求的完整生命周期 一部电影的完整剧情
Span Trace 中的一个步骤/操作 电影中的一个场景
Span Context 携带 Trace ID、Span ID 的上下文 场景编号和关联信息
Parent Span 父级 Span,表示调用者 上层场景
Child Span 子级 Span,表示被调用者 下层场景
Propagation 跨进程传递 Span Context 场景间的线索传递

一个 Trace 的结构:

复制代码
Trace: abc123
├── Span: gateway.handle_request (耗时 3200ms)
│   ├── Span: llm.call_gpt4 (耗时 1800ms)
│   │   └── Span: http.post_openai (耗时 1750ms)
│   ├── Span: tool.get_weather (耗时 400ms)
│   │   └── Span: http.call_weather_api (耗时 380ms)
│   └── Span: llm.call_gpt4 (耗时 900ms)
│       └── Span: http.post_openai (耗时 880ms)

三、Go 实现:OpenTelemetry 兼容的追踪系统

复制代码
package main

import (
	"context"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"os"
	"sync"
	"sync/atomic"
	"time"
)

// ---- 核心数据类型 ----

// SpanContext 携带跨进程传递的追踪信息
type SpanContext struct {
	TraceID    TraceID `json:"trace_id"`
	SpanID     SpanID  `json:"span_id"`
	TraceFlags byte    `json:"trace_flags"`
	TraceState string  `json:"trace_state,omitempty"`
}

type TraceID [16]byte
type SpanID [8]byte

func (t TraceID) String() string {
	return hex.EncodeToString(t[:])
}

func (s SpanID) String() string {
	return hex.EncodeToString(s[:])
}

// SpanKind 表示 Span 的角色
type SpanKind int

const (
	SpanKindInternal SpanKind = 1 // 内部调用
	SpanKindServer   SpanKind = 2 // 服务端处理请求
	SpanKindClient   SpanKind = 3 // 客户端发起请求
	SpanKindProducer SpanKind = 4 // 生产者(消息队列)
	SpanKindConsumer SpanKind = 5 // 消费者(消息队列)
)

func (k SpanKind) String() string {
	switch k {
	case SpanKindInternal:
		return "INTERNAL"
	case SpanKindServer:
		return "SERVER"
	case SpanKindClient:
		return "CLIENT"
	case SpanKindProducer:
		return "PRODUCER"
	case SpanKindConsumer:
		return "CONSUMER"
	}
	return "UNKNOWN"
}

// SpanStatus 表示 Span 的执行结果
type SpanStatus int

const (
	StatusUnset SpanStatus = 0
	StatusOK    SpanStatus = 1
	StatusError SpanStatus = 2
)

// SpanData 是 Span 的完整数据
type SpanData struct {
	TraceID      TraceID              `json:"trace_id"`
	SpanID       SpanID               `json:"span_id"`
	ParentSpanID SpanID               `json:"parent_span_id,omitempty"`
	Name         string               `json:"name"`
	Kind         SpanKind             `json:"kind"`
	StartTime    time.Time            `json:"start_time"`
	EndTime      time.Time            `json:"end_time"`
	Status       SpanStatus           `json:"status"`
	StatusDesc   string               `json:"status_description,omitempty"`
	Attributes   map[string]interface{} `json:"attributes,omitempty"`
	Events       []SpanEvent          `json:"events,omitempty"`
	Links        []SpanLink           `json:"links,omitempty"`
	Resource     map[string]string    `json:"resource,omitempty"`
}

type SpanEvent struct {
	Timestamp  time.Time              `json:"timestamp"`
	Name       string                 `json:"name"`
	Attributes map[string]interface{} `json:"attributes,omitempty"`
}

type SpanLink struct {
	TraceID    TraceID              `json:"trace_id"`
	SpanID     SpanID               `json:"span_id"`
	Attributes map[string]interface{} `json:"attributes,omitempty"`
}

// ---- TracerProvider ----
type TracerProvider struct {
	mu         sync.RWMutex
	exporters  []SpanExporter
	idGenerator IDGenerator
	resource   map[string]string
	sampler    Sampler
}

type IDGenerator interface {
	NewTraceID() TraceID
	NewSpanID() TraceID
}

type Sampler interface {
	ShouldSample(SamplingParameters) SamplingDecision
}

type SamplingParameters struct {
	TraceID      TraceID
	Name         string
	Kind         SpanKind
	Attributes   map[string]interface{}
	ParentContext SpanContext
}

type SamplingDecision struct {
	Sample      bool
	Attributes  map[string]interface{}
}

// 默认采样器:总是采样
type AlwaysOnSampler struct{}

func (s *AlwaysOnSampler) ShouldSample(params SamplingParameters) SamplingDecision {
	return SamplingDecision{Sample: true}
}

// 概率采样器
type ProbabilitySampler struct {
	probability float64
}

func NewProbabilitySampler(prob float64) *ProbabilitySampler {
	return &ProbabilitySampler{probability: prob}
}

func (s *ProbabilitySampler) ShouldSample(params SamplingParameters) SamplingDecision {
	// 基于 TraceID 的前几个字节做确定性采样
	traceID := params.TraceID
	val := binary.BigEndian.Uint64(traceID[:8])
	ratio := float64(val) / float64(^uint64(0))
	
	sample := ratio < s.probability
	return SamplingDecision{Sample: sample}
}

// 默认 ID 生成器
type DefaultIDGenerator struct {
	randomSource *rand.Rand
	mu           sync.Mutex
}

func NewDefaultIDGenerator() *DefaultIDGenerator {
	return &DefaultIDGenerator{
		randomSource: rand.New(rand.NewSource(time.Now().UnixNano())),
	}
}

func (g *DefaultIDGenerator) NewTraceID() TraceID {
	g.mu.Lock()
	defer g.mu.Unlock()
	
	var tid TraceID
	binary.BigEndian.PutUint64(tid[:8], g.randomSource.Uint64())
	binary.BigEndian.PutUint64(tid[8:], g.randomSource.Uint64())
	return tid
}

func (g *DefaultIDGenerator) NewSpanID() SpanID {
	g.mu.Lock()
	defer g.mu.Unlock()
	
	var sid SpanID
	binary.BigEndian.PutUint64(sid[:], g.randomSource.Uint64())
	return sid
}

// ---- SpanExporter ----
type SpanExporter interface {
	ExportSpans(spans []*SpanData) error
	Shutdown() error
}

// 控制台导出器
type ConsoleSpanExporter struct {
	writer io.Writer
	mu     sync.Mutex
}

func NewConsoleSpanExporter() *ConsoleSpanExporter {
	return &ConsoleSpanExporter{
		writer: os.Stdout,
	}
}

func (e *ConsoleSpanExporter) ExportSpans(spans []*SpanData) error {
	e.mu.Lock()
	defer e.mu.Unlock()

	for _, span := range spans {
		data, _ := json.MarshalIndent(span, "", "  ")
		e.writer.Write(data)
		e.writer.Write([]byte{'\n', '\n'})
	}
	return nil
}

func (e *ConsoleSpanExporter) Shutdown() error {
	return nil
}

// ---- Tracer ----
type Tracer struct {
	provider *TracerProvider
	name     string
	version  string
}

func (t *Tracer) Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, *Span) {
	config := &SpanConfig{}
	for _, opt := range opts {
		opt(config)
	}

	// 从上下文中提取父 SpanContext
	parentSpanContext := GetSpanContext(ctx)

	// 生成新的 Span ID
	spanID := t.provider.idGenerator.NewSpanID()

	// 确定 Trace ID
	var traceID TraceID
	var parentSpanID SpanID
	var isRoot bool

	if parentSpanContext.IsValid() {
		traceID = parentSpanContext.TraceID
		parentSpanID = parentSpanContext.SpanID
		isRoot = false
	} else {
		traceID = t.provider.idGenerator.NewTraceID()
		isRoot = true
	}

	// 采样决策
	samplingParams := SamplingParameters{
		TraceID:       traceID,
		Name:          name,
		Kind:          config.Kind,
		Attributes:    config.Attributes,
		ParentContext: parentSpanContext,
	}
	decision := t.provider.sampler.ShouldSample(samplingParams)

	span := &Span{
		tracer:       t,
		spanContext: SpanContext{
			TraceID: traceID,
			SpanID:  spanID,
		},
		parentSpanID: parentSpanID,
		name:         name,
		kind:         config.Kind,
		startTime:    time.Now(),
		attributes:   config.Attributes,
		events:       make([]SpanEvent, 0),
		links:        config.Links,
		resource:     t.provider.resource,
		isRecording:  decision.Sample,
		isRoot:       isRoot,
	}

	// 将新的 SpanContext 注入到上下文中
	ctx = context.WithValue(ctx, spanContextKey, span.spanContext)

	return ctx, span
}

// ---- Span ----
type Span struct {
	tracer       *Tracer
	spanContext  SpanContext
	parentSpanID SpanID
	name         string
	kind         SpanKind
	startTime    time.Time
	endTime      time.Time
	status       SpanStatus
	statusDesc   string
	attributes   map[string]interface{}
	events       []SpanEvent
	links        []SpanLink
	resource     map[string]string
	isRecording  bool
	isRoot       bool
	ended        bool
	mu           sync.Mutex
}

func (s *Span) SpanContext() SpanContext {
	return s.spanContext
}

func (s *Span) SetAttribute(key string, value interface{}) {
	if !s.isRecording {
		return
	}
	s.mu.Lock()
	if s.attributes == nil {
		s.attributes = make(map[string]interface{})
	}
	s.attributes[key] = value
	s.mu.Unlock()
}

func (s *Span) AddEvent(name string, attrs ...Attr) {
	if !s.isRecording {
		return
	}
	s.mu.Lock()
	s.events = append(s.events, SpanEvent{
		Timestamp:  time.Now(),
		Name:       name,
		Attributes: attrsToMap(attrs),
	})
	s.mu.Unlock()
}

func (s *Span) SetStatus(status SpanStatus, description string) {
	if !s.isRecording {
		return
	}
	s.mu.Lock()
	s.status = status
	s.statusDesc = description
	s.mu.Unlock()
}

func (s *Span) End(opts ...SpanEndOption) {
	if !s.isRecording {
		return
	}
	s.mu.Lock()
	if s.ended {
		s.mu.Unlock()
		return
	}
	s.ended = true
	s.endTime = time.Now()
	s.mu.Unlock()

	// 构建 SpanData 并导出
	data := &SpanData{
		TraceID:      s.spanContext.TraceID,
		SpanID:       s.spanContext.SpanID,
		ParentSpanID: s.parentSpanID,
		Name:         s.name,
		Kind:         s.kind,
		StartTime:    s.startTime,
		EndTime:      s.endTime,
		Status:       s.status,
		StatusDesc:   s.statusDesc,
		Attributes:   s.attributes,
		Events:       s.events,
		Links:        s.links,
		Resource:     s.resource,
	}

	// 异步导出
	go func() {
		for _, exporter := range s.tracer.provider.exporters {
			if err := exporter.ExportSpans([]*SpanData{data}); err != nil {
				log.Printf("[TRACE] 导出 Span 失败: %v", err)
			}
		}
	}()
}

// ---- Span 选项 ----
type SpanConfig struct {
	Kind       SpanKind
	Attributes map[string]interface{}
	Links      []SpanLink
}

type SpanOption func(*SpanConfig)
type SpanEndOption func(*SpanConfig)

func WithSpanKind(kind SpanKind) SpanOption {
	return func(c *SpanConfig) {
		c.Kind = kind
	}
}

func WithAttributes(attrs map[string]interface{}) SpanOption {
	return func(c *SpanConfig) {
		c.Attributes = attrs
	}
}

func WithLinks(links []SpanLink) SpanOption {
	return func(c *SpanConfig) {
		c.Links = links
	}
}

// ---- 上下文操作 ----
type spanContextKeyType struct{}

var spanContextKey = spanContextKeyType{}

func GetSpanContext(ctx context.Context) SpanContext {
	if ctx == nil {
		return SpanContext{}
	}
	sc, ok := ctx.Value(spanContextKey).(SpanContext)
	if !ok {
		return SpanContext{}
	}
	return sc
}

func (sc SpanContext) IsValid() bool {
	return sc.TraceID != TraceID{} && sc.SpanID != SpanID{}
}

// ---- W3C Trace Context 传播(HTTP Headers) ----
// traceparent 格式: 00-traceid-spanid-01
// tracestate 格式: vendor1=value1,vendor2=value2

func InjectSpanContextIntoHTTPHeaders(ctx context.Context, header http.Header) {
	sc := GetSpanContext(ctx)
	if !sc.IsValid() {
		return
	}

	// traceparent
	tp := fmt.Sprintf("00-%s-%s-%02x",
		sc.TraceID.String(),
		sc.SpanID.String(),
		sc.TraceFlags,
	)
	header.Set("traceparent", tp)

	// tracestate
	if sc.TraceState != "" {
		header.Set("tracestate", sc.TraceState)
	}
}

func ExtractSpanContextFromHTTPHeaders(header http.Header) SpanContext {
	// 从 traceparent 头提取
	tp := header.Get("traceparent")
	if tp == "" {
		return SpanContext{}
	}

	// 格式: 00-traceid-spanid-flags
	parts := splitTraceParent(tp)
	if len(parts) != 4 {
		return SpanContext{}
	}

	traceID, err := hex.DecodeString(parts[1])
	if err != nil || len(traceID) != 16 {
		return SpanContext{}
	}

	spanID, err := hex.DecodeString(parts[2])
	if err != nil || len(spanID) != 8 {
		return SpanContext{}
	}

	flags, _ := hex.DecodeString(parts[3])

	var tid TraceID
	var sid SpanID
	copy(tid[:], traceID)
	copy(sid[:], spanID)

	sc := SpanContext{
		TraceID:    tid,
		SpanID:     sid,
		TraceFlags: flags[0],
		TraceState: header.Get("tracestate"),
	}

	return sc
}

func splitTraceParent(tp string) []string {
	// 简单分割,实际应更健壮
	parts := make([]string, 0, 4)
	current := ""
	for _, c := range tp {
		if c == '-' {
			parts = append(parts, current)
			current = ""
		} else {
			current += string(c)
		}
	}
	parts = append(parts, current)
	return parts
}

// ---- MCP Gateway 追踪中间件 ----
type TracingMiddleware struct {
	tracer *Tracer
}

func NewTracingMiddleware(tracer *Tracer) *TracingMiddleware {
	return &TracingMiddleware{tracer: tracer}
}

func (m *TracingMiddleware) Wrap(next func(context.Context, map[string]interface{}) (interface{}, error)) func(context.Context, map[string]interface{}) (interface{}, error) {
	return func(ctx context.Context, req map[string]interface{}) (interface{}, error) {
		// 创建根 Span
		ctx, span := m.tracer.Start(ctx, "gateway.handle_request",
			WithSpanKind(SpanKindServer),
			WithAttributes(map[string]interface{}{
				"http.method": extractFromReq(req, "method"),
				"http.path":   extractFromReq(req, "path"),
				"agent.id":    extractFromReq(req, "agent_id"),
			}),
		)
		defer span.End()

		// 执行后续逻辑
		resp, err := next(ctx, req)

		if err != nil {
			span.SetStatus(StatusError, err.Error())
			span.AddEvent("error", String("error.message", err.Error()))
		} else {
			span.SetStatus(StatusOK, "")
		}

		return resp, err
	}
}

// ---- LLM 追踪装饰器 ----
type TracingLLMDecorator struct {
	tracer *Tracer
}

func NewTracingLLMDecorator(tracer *Tracer) *TracingLLMDecorator {
	return &TracingLLMDecorator{tracer: tracer}
}

func (d *TracingLLMDecorator) DecorateLLMCall(ctx context.Context, model string, prompt string, fn func() (string, int, int, error)) (string, error) {
	ctx, span := d.tracer.Start(ctx, "llm.call",
		WithSpanKind(SpanKindClient),
		WithAttributes(map[string]interface{}{
			"llm.model":          model,
			"llm.prompt_length":  len(prompt),
			"llm.prompt_preview": truncateString(prompt, 200),
		}),
	)
	defer span.End()

	response, promptTokens, completionTokens, err := fn()

	if err != nil {
		span.SetStatus(StatusError, err.Error())
		span.AddEvent("llm.error", String("error", err.Error()))
		return "", err
	}

	span.SetAttribute("llm.prompt_tokens", promptTokens)
	span.SetAttribute("llm.completion_tokens", completionTokens)
	span.SetAttribute("llm.total_tokens", promptTokens+completionTokens)
	span.SetStatus(StatusOK, "")

	return response, nil
}

// ---- 工具调用追踪装饰器 ----
type TracingToolDecorator struct {
	tracer *Tracer
}

func NewTracingToolDecorator(tracer *Tracer) *TracingToolDecorator {
	return &TracingToolDecorator{tracer: tracer}
}

func (d *TracingToolDecorator) DecorateToolCall(ctx context.Context, toolName string, args map[string]interface{}, fn func() (interface{}, error)) (interface{}, error) {
	ctx, span := d.tracer.Start(ctx, fmt.Sprintf("tool.%s", toolName),
		WithSpanKind(SpanKindClient),
		WithAttributes(map[string]interface{}{
			"tool.name": toolName,
			"tool.args": args,
		}),
	)
	defer span.End()

	result, err := fn()

	if err != nil {
		span.SetStatus(StatusError, err.Error())
		span.AddEvent("tool.error", String("error", err.Error()))
		return nil, err
	}

	span.SetAttribute("tool.success", true)
	span.SetStatus(StatusOK, "")

	return result, nil
}

// ---- 演示 ----
func main() {
	// 创建 TracerProvider
	provider := &TracerProvider{
		exporters:   []SpanExporter{NewConsoleSpanExporter()},
		idGenerator: NewDefaultIDGenerator(),
		resource: map[string]string{
			"service.name":    "mcp-gateway",
			"service.version": "1.0.0",
			"environment":     "development",
		},
		sampler: &AlwaysOnSampler{},
	}

	// 创建 Tracer
	tracer := &Tracer{
		provider: provider,
		name:     "mcp-gateway",
		version:  "1.0.0",
	}

	fmt.Println("========== 分布式追踪演示 ==========\n")

	// 1. 基本用法:创建根 Span 和子 Span
	fmt.Println("--- 1. 基本 Span 创建 ---")
	ctx := context.Background()

	// 根 Span
	ctx, rootSpan := tracer.Start(ctx, "handle_user_query",
		WithSpanKind(SpanKindServer),
		WithAttributes(map[string]interface{}{
			"user.id":   "user-xiaoming",
			"query":     "北京的天气怎么样?",
			"session":   "sess-001",
		}),
	)

	// 模拟一些工作
	time.Sleep(10 * time.Millisecond)

	// 子 Span 1:LLM 调用
	_, llmSpan := tracer.Start(ctx, "llm.call_gpt4",
		WithSpanKind(SpanKindClient),
		WithAttributes(map[string]interface{}{
			"model": "gpt-4o",
		}),
	)
	time.Sleep(100 * time.Millisecond)
	llmSpan.AddEvent("llm.response_received",
		String("tokens", "450 input, 85 output"),
	)
	llmSpan.SetStatus(StatusOK, "")
	llmSpan.End()

	// 子 Span 2:工具调用
	_, toolSpan := tracer.Start(ctx, "tool.get_weather",
		WithSpanKind(SpanKindClient),
		WithAttributes(map[string]interface{}{
			"tool": "get_weather",
			"city": "北京",
		}),
	)
	time.Sleep(50 * time.Millisecond)
	toolSpan.AddEvent("tool.api_call",
		String("endpoint", "https://api.weather.com/v1/current"),
		Int("status_code", 200),
	)
	toolSpan.SetStatus(StatusOK, "")
	toolSpan.End()

	// 结束根 Span
	rootSpan.SetStatus(StatusOK, "")
	rootSpan.End()

	// 2. 嵌套 Span(自动父子关系)
	fmt.Println("\n--- 2. 嵌套 Span ---")
	ctx2 := context.Background()

	ctx2, parentSpan := tracer.Start(ctx2, "complex_workflow",
		WithSpanKind(SpanKindServer),
	)

	// 第一层嵌套
	ctx2, child1 := tracer.Start(ctx2, "step1.validate_input")
	time.Sleep(20 * time.Millisecond)
	child1.End()

	// 第二层嵌套
	ctx2, child2 := tracer.Start(ctx2, "step2.process_with_llm")
	time.Sleep(80 * time.Millisecond)

	// 第三层嵌套(LLM 内部的 HTTP 调用)
	_, httpSpan := tracer.Start(ctx2, "http.post_to_openai",
		WithSpanKind(SpanKindClient),
	)
	time.Sleep(60 * time.Millisecond)
	httpSpan.AddEvent("http.response",
		Int("status", 200),
		Int("latency_ms", 58),
	)
	httpSpan.End()

	child2.End()
	parentSpan.End()

	// 3. 错误 Span
	fmt.Println("\n--- 3. 错误 Span ---")
	ctx3 := context.Background()

	ctx3, errorSpan := tracer.Start(ctx3, "failed_tool_call",
		WithSpanKind(SpanKindClient),
	)
	time.Sleep(30 * time.Millisecond)
	errorSpan.AddEvent("error.occurred",
		String("type", "HTTPTimeout"),
		String("message", "连接超时"),
	)
	errorSpan.SetStatus(StatusError, "HTTP 504 Gateway Timeout")
	errorSpan.End()

	// 4. 追踪中间件模拟
	fmt.Println("\n--- 4. 追踪中间件 ---")
	middleware := NewTracingMiddleware(tracer)

	handler := middleware.Wrap(func(ctx context.Context, req map[string]interface{}) (interface{}, error) {
		// 在 handler 内部创建子 Span
		ctx, innerSpan := tracer.Start(ctx, "process_request",
			WithSpanKind(SpanKindInternal),
		)
		time.Sleep(25 * time.Millisecond)
		innerSpan.End()
		return map[string]interface{}{"status": "ok"}, nil
	})

	handler(ctx, map[string]interface{}{
		"method":   "POST",
		"path":     "/api/v1/chat",
		"agent_id": "agent-001",
	})

	// 5. 展示 Trace 树形结构
	fmt.Println("\n--- 5. Trace 结构可视化 ---")
	fmt.Println("Trace Tree:")
	fmt.Println("  handle_user_query [200ms]")
	fmt.Println("  ├── llm.call_gpt4 [100ms]")
	fmt.Println("  │   └── (LLM 内部调用)")
	fmt.Println("  └── tool.get_weather [50ms]")
	fmt.Println("      └── http.call_weather_api [48ms]")

	// 6. 采样演示
	fmt.Println("\n--- 6. 概率采样 ---")
	sampler := NewProbabilitySampler(0.5) // 50% 采样率
	sampled := 0
	total := 1000

	for i := 0; i < total; i++ {
		tid := NewDefaultIDGenerator().NewTraceID()
		decision := sampler.ShouldSample(SamplingParameters{
			TraceID: tid,
			Name:    "test",
		})
		if decision.Sample {
			sampled++
		}
	}

	fmt.Printf("采样率: %.1f%% (采样 %d / 共 %d)\n",
		float64(sampled)/float64(total)*100, sampled, total)
}

四、追踪数据流架构

复制代码
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Agent Pod   │     │  Agent Pod   │     │  Agent Pod   │
│  ┌────────┐  │     │  ┌────────┐  │     │  ┌────────┐  │
│  │ Tracer │  │     │  │ Tracer │  │     │  │ Tracer │  │
│  └───┬────┘  │     │  └───┬────┘  │     │  └───┬────┘  │
└──────┼───────┘     └──────┼───────┘     └──────┼───────┘
       │                    │                    │
       └────────────────────┼────────────────────┘
                            │
                    ┌───────▼────────┐
                    │  OpenTelemetry │
                    │  Collector     │  ← 聚合、批处理、重试
                    └───────┬────────┘
                            │
              ┌─────────────┼─────────────┐
              │             │             │
      ┌───────▼──────┐ ┌───▼─────┐ ┌───▼──────┐
      │   Jaeger     │ │ Zipkin  │ │  Tempo   │  ← 追踪存储/UI
      └──────────────┘ └─────────┘ └──────────┘

五、追踪最佳实践

原则 说明 反例
每个入口创建 Trace HTTP 请求、消息消费、定时任务都要有根 Span 只在部分路径加追踪
Span 粒度适中 一个 Span 对应一个有意义的操作单元 每行代码一个 Span
携带关键属性 用户 ID、请求参数、错误信息 空 Span,什么都没有
记录事件 重要的中间状态用 Event 记录 只在 Span 结束时才记录
正确设置 Status 成功设 OK,失败设 Error 所有 Span 都是 Unset
跨服务传播 通过 HTTP Header 传递 SpanContext 每个服务独立 Trace
合理采样 高流量服务用概率采样 100% 采样导致存储爆炸

六、采样策略选择

场景 推荐采样率 理由
开发环境 100% 需要完整追踪
低流量服务(<100 QPS) 100% 存储压力小
中等流量服务(100-1000 QPS) 10%-50% 平衡成本和覆盖率
高流量服务(>1000 QPS) 1%-10% 控制存储成本
错误追踪 100%(错误时) 保证所有错误都被记录
关键用户 100%(按用户 ID 采样) 保证 VIP 用户体验

七、延伸阅读

  • OpenTelemetry Tracing Specification:OTel 追踪规范官方文档
  • W3C Trace Context:HTTP 追踪传播的标准协议
  • Jaeger 文档:Uber 开源的分布式追踪系统
  • Google Dapper 论文:分布式追踪的奠基之作
  • Sampling in Distributed Tracing:各种采样策略详解

八、下一讲预告

第3讲:指标度量(Metrics)------用数字说话

日志告诉我们发生了什么,追踪告诉我们在哪里发生的,而指标告诉我们系统处于什么状态。下一讲实现 Prometheus 兼容的指标系统:延迟分布、吞吐量、错误率、并发数,以及如何设计对 LLM Agent 有意义的业务指标。


🧰 开发之余的小工具推荐

处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

相关推荐
李溪白2 小时前
Function Calling 与 Agent 循环 —— bindTools 不会替你执行工具
agent
云烟成雨TD2 小时前
LlamaIndex 系列【29】检索增强策略:路由(Routing)机制
ai·agent·rag·llamaindex
初学AI的小高3 小时前
从LangGraph到DeepAgents:理解AgentHarness
后端·agent
KimLiu3 小时前
LCODER之AI Agent开发实战一 :问数项目智能体搭建(3)元数据知识库的构建
langchain·llm·agent
wangjialelele3 小时前
LLM Agent 全景图:MCP、ReAct、Planner、Skill 与 ANN 检索核心原理
ai·agent·hnsw·skill·ivf·mcp
Together_CZ3 小时前
在线蒸馏(OPD)、递归自我改进(RSI)与递归自我学习(RSL)整体学习理解
llm·agent·opd·rsi·在线蒸馏·rsl·递归自我学习
七夜zippoe3 小时前
Agent 输出质量保障:格式控制、校验机制与自动重试策略
ai·agent·自动重试·质量保障·格式控制·校验机制
烛之武3 小时前
LangChain笔记
langchain·大模型·agent·mcp