1. 定义全局 Tracer Provider
步骤描述:
• 初始化 OpenTelemetry 的 TracerProvider
,并配置导出器(如 Jaeger、Zipkin、Prometheus 等)以收集和导出追踪数据。
代码示例:
go 代码解读复制代码import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func initTracer() {
// 创建 Jaeger Exporter
exporter, _ := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://jaeger:14268/api/traces")))
// 配置 TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter), // 批量导出 Span
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("my-service"), // 服务名称
)),
)
// 设置为全局 TracerProvider
otel.SetTracerProvider(tp)
}
补充说明:
-
资源属性(Resource Attributes): • 必须定义服务的元数据(如
service.name
、service.version
、deployment.environment
),以便在链路追踪工具中区分不同服务和环境。 • 示例:go代码解读复制代码resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("order-service"), semconv.ServiceVersionKey.String("v1.0.0"), semconv.DeploymentEnvironmentKey.String("production"), )
-
采样策略: • 配置采样率以平衡性能和数据完整性。 • 示例:
go代码解读复制代码sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)) // 采样 10% 的请求
-
多环境支持: • 根据环境(开发、测试、生产)动态配置导出器和采样策略。
2. 添加 otelgin 和 otelhttp 中间件
步骤描述:
• 使用 OpenTelemetry 的 Gin 或 HTTP 中间件自动为每个请求创建根 Span,并记录 HTTP 相关的元数据(如方法、路径、状态码)。
代码示例(Gin):
go 代码解读复制代码import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
func main() {
r := gin.Default()
// 添加 OpenTelemetry 中间件
r.Use(otelgin.Middleware("order-service")) // 服务名称需与 TracerProvider 配置一致
r.GET("/api/orders", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello, orders!"})
})
r.Run(":8080")
}
代码示例(HTTP):
go 代码解读复制代码import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, orders!"))
})
// 包装 HTTP 处理器
handler := otelhttp.NewHandler(http.DefaultServeMux, "order-service")
http.ListenAndServe(":8080", handler)
}
补充说明:
-
服务名称一致性: • 中间件中的服务名称(如
"order-service"
)必须与TracerProvider
中配置的service.name
一致,以确保链路追踪工具正确识别服务。 -
上下文传递: • 中间件会自动将 Trace 上下文注入到 HTTP 请求头中(如
traceparent
),确保跨服务调用时上下文连续。 -
自定义属性: • 可在中间件中添加自定义属性(如用户 ID、请求 ID):
go代码解读复制代码r.Use(func(c *gin.Context) { ctx := metadata.AppendToOutgoingContext(c.Request.Context(), "user-id", "123") c.Request = c.Request.WithContext(ctx) c.Next() })
-
日志集成: • 结合 OpenTelemetry Logs SDK,将日志与 Trace 关联,提升可观测性。
-
grpc可通过服务端,客户端自动添加otel遥测
css 代码解读复制代码#服务端添加
otelHandler := otelgrpc.NewServerHandler()
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
orderService.AuthMiddleware,
),
grpc.StatsHandler(otelHandler),
)
#客户端添加
conn, err := grpc.NewClient(
config.AuthService.Addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
//添加OpenTelemetry遥测
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
- 在 gRPC 中,我们用
grpc.WithStatsHandler(otelgrpc.NewClientHandler())
自动将上下文信息(TraceContext)传递到下游服务。而在 HTTP/Gin 作为客户端 的场景下,OpenTelemetry 没有一个“自动注入中间件” ,但我们可以用一个官方推荐的方式来处理:手动使用otelhttp
封装http.Client
请求,从而自动注入 trace 信息。
Gin 中作为 HTTP 客户端调用其他服务时的做法:
使用 otelhttp
封装 http.Client
:
go 代码解读复制代码import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
var client = http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
这样创建出来的 client
,在发请求时就会自动把 trace context 写入请求头中(也就是实现了 trace 的传播)。
🧪 示例:调用其他 Gin HTTP 接口
go 代码解读复制代码req, _ := http.NewRequestWithContext(ctx, "GET", "http://inventory-service:8080/api/v1/product/123", nil)
res, err := client.Do(req)
注意:
-
ctx
应该是从当前的 gin 请求上下文中提取出来的,例如:css代码解读复制代码ctx := c.Request.Context()
-
上面的
client.Do(req)
会自动将 trace ID、span ID、baggage 等信息注入请求头中,如:traceparent
- 通过gin接口调用grpc方法,自动传递otel
背景:什么是“同一个 ctx”?
在 Go 中,context.Context
是包含 请求范围元数据(metadata) 的载体。OpenTelemetry 把 trace 信息(TraceID、SpanID 等)嵌入在这个 ctx
中,当你用这个 ctx
去调用下游服务时,otel 会自动把 trace metadata 注入到请求头里(HTTP 或 gRPC metadata)。
示例:Gin + gRPC 客户端 调用 inventory 服务
- Gin HTTP handler(order service)
go 代码解读复制代码// handler/order_handler.go
func (h *OrderHandler) CreateOrder(c *gin.Context) {
// 从 gin 的请求中获取 context(包含 trace 信息)
ctx := c.Request.Context()
// 使用这个 ctx 作为 gRPC 调用的 context
resp, err := h.InventoryClient.ReserveStock(ctx, &inventorypb.ReserveStockRequest{
ProductId: "p1",
Quantity: 1,
})
if err != nil || !resp.Success {
c.JSON(http.StatusInternalServerError, gin.H{"error": "库存预扣失败"})
return
}
// 继续处理...
c.JSON(http.StatusOK, gin.H{"message": "订单创建成功"})
}
关键点:
- 使用
ctx := c.Request.Context()
保留了 trace 上下文 - 把这个
ctx
原封不动传给下游gRPC
客户端
- gRPC 客户端初始化(order service)
go 代码解读复制代码// internal/client/inventory_client.go
func InitInventoryClient() (inventorypb.InventoryServiceClient, error) {
conn, err := grpc.NewClient(
"inventory-service:50052",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()), // 自动从 ctx 中提取 trace 信息
)
if err != nil {
return nil, err
}
return inventorypb.NewInventoryServiceClient(conn), nil
}
- gRPC 服务端(inventory service)
css 代码解读复制代码// main.go
grpcServer := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()), // 自动解析 trace 信息
)
inventorypb.RegisterInventoryServiceServer(grpcServer, inventoryServer)
链路完整图(Jaeger中可视化)
scss 代码解读复制代码[POST /create-order]
↓ gin middleware (otelgin)
↓ ctx 含 trace 信息
[gRPC call → inventory]
↓ otelgrpc client handler
↓ metadata 写入 trace
[Inventory gRPC 服务端]
↓ otelgrpc server handler
↓ trace 继续传递
总结
步骤 | 动作 | 保证了什么 |
---|---|---|
ctx := c.Request.Context() | 从 HTTP 获取 trace 信息 | 保证 trace 上下文起点 |
grpcClient.Call(ctx, ...) | 将 trace 信息传递给 gRPC | 保证 trace 传播 |
otelgrpc.NewClientHandler() | 自动打包 trace 到 metadata | 无需手动写入 trace ID |
otelgrpc.NewServerHandler() | 自动解包 metadata → ctx | 服务端继续追踪 |
3. 在服务调用其他服务时通过 otel.Tracer.Start
创建子 Span
步骤描述:
• 在跨服务调用时,手动创建子 Span,并将 Trace 上下文注入到请求头中,确保调用链的连续性。
代码示例(gRPC):
go 代码解读复制代码func (s *OrderService) CallAnotherService(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// 创建子 Span
ctx, span := otel.Tracer("call-another-service").Start(
ctx,
"call-another-service",
trace.WithSpanKind(trace.SpanKindClient), // 标记为客户端调用
)
defer span.End()
// 将 Trace 上下文注入到 gRPC 元数据中
md, _ := metadata.FromOutgoingContext(ctx)
ctx = metadata.NewOutgoingContext(ctx, metadata.Join(md, propagation.HeaderCarrier(propagator)))
// 调用远程服务
client := pb.NewAnotherServiceClient(s.conn)
return client.AnotherMethod(ctx, req)
}
代码示例(HTTP):
go 代码解读复制代码func CallExternalAPI(ctx context.Context, url string) (*http.Response, error) {
// 创建子 Span
ctx, span := otel.Tracer("call-external-api").Start(
ctx,
"call-external-api",
trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()
// 将 Trace 上下文注入到 HTTP 请求头中
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
// 发送请求
client := &http.Client{}
return client.Do(req)
}
补充说明:
-
Span 类型: • 使用
trace.SpanKindClient
标记客户端调用,确保链路追踪工具正确识别调用方向。 -
上下文注入: • 使用 OpenTelemetry 的
propagator
将 Trace 上下文注入到请求头中(如 gRPC 的metadata
或 HTTP 的Header
)。 • 示例(HTTP):go代码解读复制代码propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
-
跨语言支持: • 确保所有服务使用相同的 Propagator(如 W3C TraceContext),以实现跨语言链路追踪。
-
错误处理: • 在子 Span 中记录错误状态和耗时,便于排查问题。
4. 补充步骤和建议
虽然上述步骤已覆盖核心流程,但仍有一些补充步骤和优化建议:
(1) 初始化 Propagator
• 确保正确配置上下文传播器(Propagator),以支持跨服务调用时的 Trace 上下文传递。 • 示例:
go 代码解读复制代码import "go.opentelemetry.io/otel/propagation"
func init() {
propagator := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, // W3C TraceContext
propagation.Baggage{}, // Baggage
)
otel.SetTextMapPropagator(propagator)
}
(2) 添加 Metrics 和 Logs
• Metrics: • 使用 OpenTelemetry Metrics SDK 收集服务性能指标(如请求延迟、错误率)。 • 示例: ```go import "go.opentelemetry.io/otel/metric"
css 代码解读复制代码meter := otel.Meter("order-service")
requestCounter := meter.Int64Counter("requests_total")
requestCounter.Add(ctx, 1)
```
• Logs: • 使用 OpenTelemetry Logs SDK 记录结构化日志,并与 Trace 关联。 • 示例: ```go import "go.opentelemetry.io/otel/sdk/log"
css 代码解读复制代码logger := log.NewLogger(log.WithSink(os.Stdout))
logger.Log(context.Background(), "info", "request received")
```
(3) 配置 Exporter
• 根据需求选择合适的 Exporter(如 Jaeger、Zipkin、Prometheus、OTLP)。 • 示例(OTLP):
go代码解读复制代码exporter, _ := otlp.NewExporter(otlp.WithInsecure()) tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
(4) 动态配置
• 使用环境变量或配置文件动态设置服务名称、采样率和导出器。 • 示例:
go 代码解读复制代码serviceName := os.Getenv("SERVICE_NAME")
if serviceName == "" {
serviceName = "default-service"
}
(5) 测试和验证
• 使用链路追踪工具(如 Jaeger、Zipkin)验证 Span 是否正确生成和传递。 • 检查 Trace 的完整性(如服务间调用链是否连续)。
完整流程总结
步骤 | 描述 | 工具/组件 |
---|---|---|
1. 初始化 Tracer Provider | 配置全局 Tracer Provider 和 Exporter,定义资源属性和采样策略。 | sdktrace.NewTracerProvider |
2. 添加中间件 | 使用 otelgin.Middleware 或 otelhttp 自动创建根 Span,记录 HTTP 元数据。 | otelgin 或 otelhttp |
3. 创建子 Span | 在跨服务调用时手动创建子 Span,并注入 Trace 上下文。 | otel.Tracer.Start |
4. 配置 Propagator | 确保 Trace 上下文在服务间正确传递。 | propagation.NewCompositeTextMapPropagator |
5. 添加 Metrics 和 Logs | 收集性能指标和结构化日志,增强可观测性。 | otel.Meter , otel/sdk/log |
6. 动态配置和测试 | 根据环境动态配置 Tracer,并验证链路追踪数据的完整性。 | 环境变量、测试工具 |
最终建议
• 核心流程:上述步骤已覆盖单个服务添加 OpenTelemetry 的主要流程。 • 补充优化:根据实际需求,添加 Propagator 配置、Metrics、Logs 和动态配置功能,进一步提升可观测性。 • 验证和监控:通过链路追踪工具(如 Jaeger)和监控系统(如 Prometheus)验证和优化链路追踪数据。
通过以上补充,可以确保单个服务的 OpenTelemetry 实现更加完整和健壮。
评论记录:
回复评论: