微服务添加 OpenTelemetry (otel)

426 阅读7分钟

1. 定义全局 Tracer Provider

步骤描述

• 初始化 OpenTelemetry 的 TracerProvider,并配置导出器(如 Jaeger、Zipkin、Prometheus 等)以收集和导出追踪数据。

代码示例

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)
}

补充说明

  1. 资源属性(Resource Attributes): • 必须定义服务的元数据(如 service.nameservice.versiondeployment.environment),以便在链路追踪工具中区分不同服务和环境。 • 示例:

    resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceNameKey.String("order-service"),
        semconv.ServiceVersionKey.String("v1.0.0"),
        semconv.DeploymentEnvironmentKey.String("production"),
    )
    
  2. 采样策略: • 配置采样率以平衡性能和数据完整性。 • 示例:

    sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)) // 采样 10% 的请求
    
  3. 多环境支持: • 根据环境(开发、测试、生产)动态配置导出器和采样策略。


2. 添加 otelgin 和 otelhttp 中间件

步骤描述

• 使用 OpenTelemetry 的 Gin 或 HTTP 中间件自动为每个请求创建根 Span,并记录 HTTP 相关的元数据(如方法、路径、状态码)。

代码示例(Gin)

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)

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)
}

补充说明

  1. 服务名称一致性: • 中间件中的服务名称(如 "order-service")必须与 TracerProvider 中配置的 service.name 一致,以确保链路追踪工具正确识别服务。

  2. 上下文传递: • 中间件会自动将 Trace 上下文注入到 HTTP 请求头中(如 traceparent),确保跨服务调用时上下文连续。

  3. 自定义属性: • 可在中间件中添加自定义属性(如用户 ID、请求 ID):

    r.Use(func(c *gin.Context) {
        ctx := metadata.AppendToOutgoingContext(c.Request.Context(), "user-id", "123")
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    })
    
  4. 日志集成: • 结合 OpenTelemetry Logs SDK,将日志与 Trace 关联,提升可观测性。

  5. grpc可通过服务端,客户端自动添加otel遥测

#服务端添加
	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()),
	)
  1. 在 gRPC 中,我们用 grpc.WithStatsHandler(otelgrpc.NewClientHandler()) 自动将上下文信息(TraceContext)传递到下游服务。而在 HTTP/Gin 作为客户端 的场景下,OpenTelemetry 没有一个“自动注入中间件” ,但我们可以用一个官方推荐的方式来处理:手动使用 otelhttp 封装 http.Client 请求,从而自动注入 trace 信息。

Gin 中作为 HTTP 客户端调用其他服务时的做法:

使用 otelhttp 封装 http.Client

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 接口

req, _ := http.NewRequestWithContext(ctx, "GET", "http://inventory-service:8080/api/v1/product/123", nil)
res, err := client.Do(req)

注意:

  • ctx 应该是从当前的 gin 请求上下文中提取出来的,例如:

    ctx := c.Request.Context()
    
  • 上面的 client.Do(req) 会自动将 trace ID、span ID、baggage 等信息注入请求头中,如:traceparent

  1. 通过gin接口调用grpc方法,自动传递otel

背景:什么是“同一个 ctx”?

在 Go 中,context.Context 是包含 请求范围元数据(metadata) 的载体。OpenTelemetry 把 trace 信息(TraceID、SpanID 等)嵌入在这个 ctx 中,当你用这个 ctx 去调用下游服务时,otel 会自动把 trace metadata 注入到请求头里(HTTP 或 gRPC metadata)。


示例:Gin + gRPC 客户端 调用 inventory 服务

  1. Gin HTTP handler(order service)
// 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 客户端

  1. gRPC 客户端初始化(order service)
// 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
}

  1. gRPC 服务端(inventory service)
// main.go
grpcServer := grpc.NewServer(
    grpc.StatsHandler(otelgrpc.NewServerHandler()), // 自动解析 trace 信息
)
inventorypb.RegisterInventoryServiceServer(grpcServer, inventoryServer)

链路完整图(Jaeger中可视化)

[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)

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)

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)
}

补充说明

  1. Span 类型: • 使用 trace.SpanKindClient 标记客户端调用,确保链路追踪工具正确识别调用方向。

  2. 上下文注入: • 使用 OpenTelemetry 的 propagator 将 Trace 上下文注入到请求头中(如 gRPC 的 metadata 或 HTTP 的 Header)。 • 示例(HTTP):

    propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
    
  3. 跨语言支持: • 确保所有服务使用相同的 Propagator(如 W3C TraceContext),以实现跨语言链路追踪。

  4. 错误处理: • 在子 Span 中记录错误状态和耗时,便于排查问题。


4. 补充步骤和建议

虽然上述步骤已覆盖核心流程,但仍有一些补充步骤和优化建议:

(1) 初始化 Propagator

• 确保正确配置上下文传播器(Propagator),以支持跨服务调用时的 Trace 上下文传递。 • 示例:

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"

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"

logger := log.NewLogger(log.WithSink(os.Stdout))
logger.Log(context.Background(), "info", "request received")
```

(3) 配置 Exporter

• 根据需求选择合适的 Exporter(如 Jaeger、Zipkin、Prometheus、OTLP)。 • 示例(OTLP):

exporter, _ := otlp.NewExporter(otlp.WithInsecure())
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))

(4) 动态配置

• 使用环境变量或配置文件动态设置服务名称、采样率和导出器。 • 示例:

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.Middlewareotelhttp 自动创建根 Span,记录 HTTP 元数据。otelginotelhttp
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 实现更加完整和健壮。