一、可观测性三大支柱

可观测性(Observability)包含三个维度:追踪(Traces)、指标(Metrics)、日志(Logs)。分布式追踪是排查跨服务调用链路问题的核心工具。

1.1 追踪模型:OpenTelemetry

// OpenTelemetry(OTel)的核心概念:
//
// Trace(追踪):一次完整的分布式请求
//   ├── Span(跨度):一个服务/组件的工作单元
//   │   ├── span.kind:client / server / producer / consumer / internal
//   │   ├── span.name:操作名(如 HTTP GET / DB query)
//   │   ├── span.attributes:键值对元数据
//   │   └── span.status:OK / ERROR
//   │
//   └── SpanContext(跨度上下文):
//       ├── trace_id:全局唯一(决定属于哪个Trace)
//       ├── span_id:当前Span唯一
//       └── trace_flags:采样标志

// OpenTelemetry Go SDK使用:
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer() (*trace.TracerProvider, error) {
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://jaeger:14268/api/traces"),
    ))
    if err != nil {
        return nil, err
    }

    tp := trace.NewTracerProvider(
        trace.WithBatcher(exp),
        // 采样策略:
        // - AlwaysOn:全部采样
        // - AlwaysOff:全部不采样
        // - TraceIdRatioBased:按比例采样
        // - ParentBased:继承父Span的采样决策
        trace.WithSampler(trace.TraceIdRatioBased(0.1)), // 10%采样
    )

    otel.SetTracerProvider(tp)
    return tp, nil
}

二、Span的创建与传播

2.1 自动注入与手动创建

// 方法一:Gin中间件自动追踪
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

func main() {
    r := gin.Default()

    r.Use(otelgin.Middleware("my-service",
        otelgin.WithTracerProvider(tp),
    ))

    r.GET("/api/orders/:id", func(c *gin.Context) {
        // 自动创建Span,追踪整个HTTP请求
        order, err := getOrder(c.Param("id"))
        c.JSON(200, order)
    })

    r.Run(":8080")
}

// 方法二:手动Span创建(精确控制范围)
func processOrder(ctx context.Context, orderID string) error {
    // 从context中提取父Span
    tracer := otel.Tracer("order-service")

    ctx, span := tracer.Start(ctx, "processOrder",
        trace.WithAttributes(
            attribute.String("order.id", orderID),
            attribute.String("order.service", "order-processor"),
        ),
        trace.WithSpanKind(trace.SpanKindServer),
    )
    defer span.End() // 结束时自动记录duration

    // 业务逻辑
    if err := validateOrder(ctx, orderID); err != nil {
        span.RecordError(err)        // 记录错误
        span.SetStatus(codes.Error, err.Error())
        return err
    }

    // 创建子Span(DB调用)
    ctx, dbSpan := tracer.Start(ctx, "db.query",
        trace.WithAttributes(
            attribute.String("db.system", "mysql"),
            attribute.String("db.statement", "SELECT * FROM orders WHERE id=?"),
        ),
    )
    order, err := db.QueryOrder(ctx, orderID)
    dbSpan.End()
    if err != nil {
        dbSpan.RecordError(err)
    }

    span.SetAttributes(attribute.String("order.status", order.Status))
    return nil
}

2.2 TraceContext传播

// TraceContext在HTTP Header中的传播
// W3C TraceContext标准:
// traceparent: 00-{trace_id}-{span_id}-{trace_flags}
// traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
//                                        ↑      ↑                  ↑
//                                    trace_id span_id          flags(01=采样)

func callDownstream(ctx context.Context, url string) (*http.Response, error) {
    // 从当前context提取span
    tracer := otel.Tracer("http-client")

    ctx, span := tracer.Start(ctx, "HTTP GET",
        trace.WithSpanKind(trace.SpanKindClient),
    )
    defer span.End()

    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)

    // 注入TraceContext到HTTP Header
    // otel.GetTextMapPropagator().Inject(ctx, carrier)
    // carrier实现了TextMapCarrier接口(就是http.Request.Header)
    otel.GetTextMapPropagator().Inject(ctx, req.Header)

    span.SetAttributes(
        attribute.String("http.url", url),
        attribute.String("http.method", "GET"),
    )

    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        span.RecordError(err)
        return resp, err
    }

    span.SetAttributes(
        attribute.Int("http.status_code", resp.StatusCode),
    )

    return resp, nil
}

// 在接收端恢复Trace:
func receiveRequest(c *gin.Context) {
    // 从HTTP Header提取TraceContext
    ctx := otel.GetTextMapPropagator().Extract(
        c.Request.Context(),
        propagation.HeaderCarrier(c.Request.Header),
    )

    // 继续创建子Span,自动关联到原始Trace
    tracer := otel.Tracer("receiver-service")
    _, span := tracer.Start(ctx, "handleRequest",
        trace.WithSpanKind(trace.SpanKindServer),
    )
    defer span.End()

    // 整个调用链的trace_id保持一致
}

三、采样策略与性能优化

// 采样策略选择指南:
//
// ① Head-based Sampling(请求入口采样)
//    优点:简单,入口决定,资源可控
//    缺点:低频请求可能被丢弃(重要请求恰好没采到)
//
// ② Tail-based Sampling(尾部采样)
//    优点:可以根据结果决策(错误请求必采)
//    缺点:需要额外的Collector(如OpenTelemetry Collector)

// Head-based采样配置(适配不同环境):
func newTracerProvider(serviceName string, samplingRate float64) (*trace.TracerProvider, error) {
    var sampler trace.Sampler

    switch {
    case os.Getenv("ENV") == "production":
        // 生产:极低采样率 + 错误请求全采
        sampler = trace.ParentBased(
            trace.TraceIdRatioBased(0.01), // 1%采样
        )
    case os.Getenv("ENV") == "staging":
        sampler = trace.TraceIdRatioBased(0.1) // 10%采样
    default:
        sampler = trace.AlwaysSample() // 开发环境全采样
    }

    return trace.NewTracerProvider(
        trace.WithSampler(sampler),
    ), nil
}

// OpenTelemetry Collector配置(尾部采样):
// otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 1000 }
      - name: sampled-traces
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

exporters:
  jaeger:
    endpoint: jaeger:14250

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling]
      exporters: [jaeger]