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 之间的父子关系。
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 层级。