4. 基础链路追踪:在 Go HTTP 服务中创建第一个 Trace 和 Span,输出到控制台

链路追踪的起点是理解 Trace 和 Span 这两个核心抽象。本章将带领你在一个最简单的 Go HTTP 服务中手动创建 Trace 和 Span,并将追踪数据直接输出到控制台,从而直观地观察链路的结构与生命周期。

4.1 Trace 与 Span 的核心概念

在 OpenTelemetry 中,一次完整的请求处理过程被抽象为一个 Trace(追踪),而 Trace 由多个 Span(跨度)组成。每个 Span 代表一个具有开始时间和结束时间的操作单元,例如一次数据库查询、一次外部 HTTP 调用或一个函数执行。

概念 说明 类比
Trace 一次完整请求的全局视图,由唯一 TraceID 标识 一次完整的快递配送过程
Span Trace 中的一个独立操作单元,由 SpanID 标识,包含父 SpanID 形成层级 配送过程中的“揽件”、“运输”、“派送”等环节
SpanContext 携带 TraceID、SpanID 等追踪元数据的上下文对象 快递单上的追踪号码
关键理解: Trace 是 Span 的有向无环图(DAG)。根 Span(Root Span)是整个 Trace 的入口,没有父 SpanID;子 Span 通过父 SpanID 与根 Span 关联。

4.2 第一个 Trace 的完整结构

下面的 SVG 图展示了一个包含两个 Span 的简单 Trace 结构:一个根 Span 代表 HTTP 请求处理,一个子 Span 代表业务逻辑处理。箭头表示时间流向,虚线框表示 Span 之间的父子关系。

第一个 Trace 结构:根 Span + 子 Span 时间→ Trace (TraceID: abc123) Root Span: HTTP GET /api/order SpanID: span-001 | 父SpanID: 无 Child Span: 查询订单数据 SpanID: span-002 | 父SpanID: span-001 父→子 整个 Trace 的时间跨度

4.3 环境准备与依赖安装

在开始编码之前,需要安装 OpenTelemetry Go SDK 的核心包。本示例使用 go.opentelemetry.io/otel 及其相关模块。

# 初始化 Go 模块
go mod init trace-demo

# 安装 OpenTelemetry 核心包
go get go.opentelemetry.io/otel
go get go.opentemetry.io/otel/trace
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/stdout/stdouttrace
依赖说明:
  • otel:核心 API,包含 TracerProvider、Tracer 等接口。
  • otel/trace:Span 创建和操作的核心接口。
  • otel/sdk/trace:SDK 实现,负责 Span 的采样、处理和导出。
  • otel/exporters/stdout/stdouttrace:将 Span 数据输出到标准输出的导出器,适合开发和调试。

4.4 代码实现:创建 Trace 并输出到控制台

以下代码实现了一个简单的 HTTP 服务,在请求处理过程中手动创建根 Span 和一个子 Span,并将追踪数据通过 stdouttrace 导出器输出到控制台。

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
    "go.opentelemetry.io/otel/trace"
)

// 初始化 TracerProvider,使用 stdout 导出器
func initTracer() (*sdktrace.TracerProvider, error) {
    // 创建 stdout 导出器,输出到控制台
    exporter, err := stdouttrace.New(
        stdouttrace.WithPrettyPrint(),
    )
    if err != nil {
        return nil, err
    }

    // 创建资源,标识服务名称
    res, err := resource.New(context.Background(),
        resource.WithAttributes(
            semconv.ServiceNameKey.String("order-service"),
        ),
    )
    if err != nil {
        return nil, err
    }

    // 创建 TracerProvider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
    )
    return tp, nil
}

// 业务处理函数:模拟查询订单
func processOrder(ctx context.Context, orderID string) {
    // 从全局 TracerProvider 获取 Tracer
    tracer := otel.Tracer("order-service")

    // 创建子 Span,继承父 Span 的上下文
    _, span := tracer.Start(ctx, "查询订单数据",
        trace.WithAttributes(
            attribute.String("order.id", orderID),
            attribute.String("db.type", "MySQL"),
        ),
    )
    defer span.End()

    // 模拟业务耗时
    time.Sleep(50 * time.Millisecond)
    span.AddEvent("订单数据查询完成")
}

// HTTP 处理函数
func orderHandler(w http.ResponseWriter, r *http.Request) {
    // 从全局 TracerProvider 获取 Tracer
    tracer := otel.Tracer("order-service")

    // 创建根 Span,代表整个 HTTP 请求
    ctx, span := tracer.Start(r.Context(), "HTTP GET /api/order",
        trace.WithAttributes(
            attribute.String("http.method", r.Method),
            attribute.String("http.url", r.URL.String()),
        ),
    )
    defer span.End()

    // 模拟请求处理
    orderID := r.URL.Query().Get("id")
    if orderID == "" {
        orderID = "unknown"
    }

    // 调用子 Span 的业务逻辑
    processOrder(ctx, orderID)

    // 设置 Span 状态
    span.SetStatus(sdktrace.Status{Code: sdktrace.StatusCodeOk})
    span.AddEvent("请求处理完成")

    fmt.Fprintf(w, "Order processed: %s", orderID)
}

func main() {
    // 初始化 TracerProvider
    tp, err := initTracer()
    if err != nil {
        log.Fatal(err)
    }
    defer tp.Shutdown(context.Background())

    // 设置全局 TracerProvider
    otel.SetTracerProvider(tp)

    // 注册 HTTP 路由
    http.HandleFunc("/api/order", orderHandler)

    log.Println("服务启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

4.5 运行与观察控制台输出

启动服务后,访问 http://localhost:8080/api/order?id=1001,控制台将输出类似以下的追踪数据(经过格式化):

{
  "Name": "HTTP GET /api/order",
  "SpanContext": {
    "TraceID": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "SpanID": "span-001",
    "TraceFlags": "01"
  },
  "Parent": {
    "SpanID": "0000000000000000"
  },
  "StartTime": "2025-01-15T10:30:00.123456Z",
  "EndTime": "2025-01-15T10:30:00.223456Z",
  "Attributes": [
    {"Key": "http.method", "Value": {"Type": "STRING", "Value": "GET"}},
    {"Key": "http.url", "Value": {"Type": "STRING", "Value": "/api/order?id=1001"}}
  ],
  "Events": [
    {
      "Name": "请求处理完成",
      "Timestamp": "2025-01-15T10:30:00.223456Z"
    }
  ],
  "ChildSpanCount": 1
}
{
  "Name": "查询订单数据",
  "SpanContext": {
    "TraceID": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "SpanID": "span-002",
    "TraceFlags": "01"
  },
  "Parent": {
    "SpanID": "span-001"
  },
  "StartTime": "2025-01-15T10:30:00.123456Z",
  "EndTime": "2025-01-15T10:30:00.173456Z",
  "Attributes": [
    {"Key": "order.id", "Value": {"Type": "STRING", "Value": "1001"}},
    {"Key": "db.type", "Value": {"Type": "STRING", "Value": "MySQL"}}
  ],
  "Events": [
    {
      "Name": "订单数据查询完成",
      "Timestamp": "2025-01-15T10:30:00.173456Z"
    }
  ],
  "ChildSpanCount": 0
}
观察要点:
  • 两个 Span 共享同一个 TraceID,证明它们属于同一次请求。
  • 根 Span 的 Parent.SpanID 为全零,表示没有父 Span。
  • 子 Span 的 Parent.SpanID 指向根 Span 的 SpanID,形成父子关系。
  • StartTime 和 EndTime 精确记录了每个 Span 的耗时。

4.6 关键代码解析

代码片段 作用
otel.SetTracerProvider(tp) 将自定义的 TracerProvider 设置为全局实例,后续所有 otel.Tracer() 调用都使用此 Provider。
tracer.Start(ctx, "span名称", ...) 在给定上下文中创建一个新的 Span。如果上下文已包含 Span,则新 Span 自动成为其子 Span。
defer span.End() 确保 Span 在函数退出时结束,记录正确的耗时。
span.SetAttributes(...) 为 Span 添加键值对属性,用于描述操作的特征(如 HTTP 方法、订单 ID)。
span.AddEvent("事件名") 在 Span 的生命周期内添加时间戳事件,用于标记关键节点(如“查询完成”)。
span.SetStatus(...) 设置 Span 的结束状态(OK / Error),用于标识操作是否成功。

4.7 常见问题与避坑指南

⚠️ 常见错误:
  • 忘记调用 span.End():Span 不会自动结束,会导致追踪数据不完整或内存泄漏。务必使用 defer span.End()。
  • 上下文传递断裂:在调用子函数时,必须传递包含 Span 的 context.Context,否则子 Span 无法关联到父 Trace。
  • 未设置全局 TracerProvider:如果忘记调用 otel.SetTracerProvider(tp),otel.Tracer() 将使用默认的无操作(Noop)实现,不会产生任何追踪数据。
  • 导出器未刷新:使用批处理导出器时,程序退出前应调用 tp.Shutdown() 确保所有 Span 被导出。

4.8 本章小结

通过本章实践,你已经完成了以下关键步骤:

  • 理解了 Trace 和 Span 的基本概念及其父子关系。
  • 使用 OpenTelemetry Go SDK 初始化了 TracerProvider 和 stdout 导出器。
  • 在 HTTP 服务中手动创建了根 Span 和子 Span,并添加了属性和事件。
  • 观察了控制台输出的 JSON 格式追踪数据,验证了 TraceID 一致性和 Span 层级。