Go HTTP 鏈路追蹤

Open-Telemetry 的第三方軟件包合集 包括了多個社區中常用庫的 OpenTelemetry 支持。隨着 OpenTelemetry 的不斷迭代,相信整個鏈路追蹤的生態也會越發完善。

基於 OTel 的 HTTP 鏈路追蹤

基於 OTel 的 HTTP 客戶端和服務端鏈路追蹤實踐。

客戶端

實現 HTTP client 的鏈路追蹤。

package main

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

 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 "go.opentelemetry.io/otel"
 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 "go.opentelemetry.io/otel/sdk/resource"
 sdktrace "go.opentelemetry.io/otel/sdk/trace"
 semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
 "go.opentelemetry.io/otel/trace"
)

// HTTP Client

const (
 serviceName     = "httpclient-Demo"
 peerServiceName = "blog"
 jaegerEndpoint  = "127.0.0.1:4318"
 blogURL         = "https://liwenzhou.com"
)

// newJaegerTraceProvider 創建一個 Jaeger Trace Provider
func newJaegerTraceProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
 // 創建一個使用 HTTP 協議連接本機Jaeger的 Exporter
 exp, err := otlptracehttp.New(ctx,
  otlptracehttp.WithEndpoint(jaegerEndpoint),
  otlptracehttp.WithInsecure())
 if err != nil {
  return nil, err
 }
 res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)))
 if err != nil {
  return nil, err
 }
 // 創建 Provider
 traceProvider := sdktrace.NewTracerProvider(
  sdktrace.WithResource(res),
  sdktrace.WithSampler(sdktrace.AlwaysSample()), // 採樣
  sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)),
 )
 return traceProvider, nil
}

func main() {
 ctx := context.Background()
 tp, err := newJaegerTraceProvider(ctx)
 if err != nil {
  log.Fatal(err)
 }
 defer func() {
  _ = tp.Shutdown(context.Background())
 }()

 // 創建 tracer
 tr := otel.Tracer("http-client")
 // 開啓 span,PeerService 指要連接的目標服務
 ctx, span := tr.Start(ctx, "blog", trace.WithAttributes(semconv.PeerService(peerServiceName)))
 defer span.End()

 // 構建請求
 req, _ := http.NewRequestWithContext(ctx, http.MethodGet, blogURL, nil)

 client := http.Client{
  Transport: otelhttp.NewTransport(http.DefaultTransport),
 }
 // 發送請求
 res, _ := client.Do(req)

 body, err := io.ReadAll(res.Body)
 _ = res.Body.Close()

 fmt.Printf("Response Received: %s\n", body)
}

要深入net/http內部的追蹤可以使用net/http/httptrace

package main

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

 "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 "go.opentelemetry.io/otel"
 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 "go.opentelemetry.io/otel/sdk/resource"
 sdktrace "go.opentelemetry.io/otel/sdk/trace"
 semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
 "go.opentelemetry.io/otel/trace"
)

// HTTP Client

const (
 serviceName     = "httpclient-Demo"
 peerServiceName = "blog"
 jaegerEndpoint  = "127.0.0.1:4318"
 blogURL         = "https://liwenzhou.com"
)

// newJaegerTraceProvider 創建一個 Jaeger Trace Provider
func newJaegerTraceProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
 // 創建一個使用 HTTP 協議連接本機Jaeger的 Exporter
 exp, err := otlptracehttp.New(ctx,
  otlptracehttp.WithEndpoint(jaegerEndpoint),
  otlptracehttp.WithInsecure())
 if err != nil {
  return nil, err
 }
 res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)))
 if err != nil {
  return nil, err
 }
 // 創建 Provider
 traceProvider := sdktrace.NewTracerProvider(
  sdktrace.WithResource(res),
  sdktrace.WithSampler(sdktrace.AlwaysSample()), // 採樣
  sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)),
 )
 return traceProvider, nil
}

func main() {
 ctx := context.Background()
 tp, err := newJaegerTraceProvider(ctx)
 if err != nil {
  log.Fatal(err)
 }
 defer func() {
  _ = tp.Shutdown(context.Background())
 }()

 // 創建tracer
 tr := otel.Tracer("http-client")
 // 開啓 span,PeerService 指要連接的目標服務
 ctx, span := tr.Start(ctx, "blog", trace.WithAttributes(semconv.PeerService(peerServiceName)))
 defer span.End()

 // 創建 http client,配置trace
 client := http.Client{
  Transport: otelhttp.NewTransport(
   http.DefaultTransport,
   otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
    return otelhttptrace.NewClientTrace(ctx)
   }),
  ),
 }
 ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
 // 構建請求
 req, _ := http.NewRequestWithContext(ctx, http.MethodGet, blogURL, nil)
 // 發送請求
 res, _ := client.Do(req)

 body, err := io.ReadAll(res.Body)
 _ = res.Body.Close()

 fmt.Printf("Response Received: %s\n", body)
}

服務端

net/http 服務端的 trace 配置在之前的教程介紹過。

uk := attribute.Key("username")

helloHandler := func(w http.ResponseWriter, req *http.Request) {
  ctx := req.Context()
  span := trace.SpanFromContext(ctx)
  bag := baggage.FromContext(ctx)
  span.AddEvent("handling this...", trace.WithAttributes(uk.String(bag.Member("username").Value())))

  _, _ = io.WriteString(w, "Hello, world!\n")
}

otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler)"Hello")

http.Handle("/hello", otelHandler)

gin 框架

我們通常做 Web 開發都是使用 gin 框架,gin 框架使用otelgin庫提供 trace 能力。

安裝依賴:

go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin

然後在代碼中註冊相應的中間件。

// 設置 otelgin 中間件
r.Use(otelgin.Middleware(serviceName))

如果需要將 traceID 以響應頭的方式返回給前端,可以添加以下中間件。

注意:

  1. 不能直接傳遞 gin 框架的 gin.Context,需要傳遞 http.Request 中內置的 context.Context

  2. 響應頭中的 traceID 格式爲Trace-Id:25725adb30f61833bdf09806944ee2a4

// 在響應頭記錄 TRACE-ID
r.Use(func(c *gin.Context) {
  c.Header("Trace-Id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String())
})

完整示例代碼:

package main

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

 "github.com/gin-gonic/gin"
 "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
 "go.opentelemetry.io/otel"
 "go.opentelemetry.io/otel/attribute"
 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 "go.opentelemetry.io/otel/propagation"
 "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"
 "go.uber.org/zap"
)

const (
 serviceName    = "Gin-Jaeger-Demo"
 jaegerEndpoint = "127.0.0.1:4318"
)

var tracer = otel.Tracer("gin-server")

func main() {
 ctx := context.Background()

 // 初始化並配置 Tracer
 tp, err := initTracer(ctx)
 if err != nil {
  log.Fatal("initTracer failed", zap.Error(err))
 }
 defer func() {
  if err := tp.Shutdown(ctx); err != nil {
   log.Fatal("Error shutting down tracer provider", zap.Error(err))
  }
 }()

 r := gin.New()

 // 設置 otelgin 中間件
 r.Use(otelgin.Middleware(serviceName))

 // 在響應頭記錄 TRACE-ID
 r.Use(func(c *gin.Context) {
  c.Header("Trace-Id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String())
 })

 r.GET("/users/:id", func(c *gin.Context) {
  id := c.Param("id")
  name := getUser(c, id)
  c.JSON(http.StatusOK, gin.H{
   "name": name,
   "id":   id,
  })
 })
 _ = r.Run(":8080")
}

// newJaegerTraceProvider 創建一個 Jaeger Trace Provider
func newJaegerTraceProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
 // 創建一個使用 HTTP 協議連接本機Jaeger的 Exporter
 exp, err := otlptracehttp.New(ctx,
  otlptracehttp.WithEndpoint(jaegerEndpoint),
  otlptracehttp.WithInsecure())
 if err != nil {
  return nil, err
 }
 res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)))
 if err != nil {
  return nil, err
 }
 traceProvider := sdktrace.NewTracerProvider(
  sdktrace.WithResource(res),
  sdktrace.WithSampler(sdktrace.AlwaysSample()), // 採樣
  sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)),
 )
 return traceProvider, nil
}

// initTracer 初始化 Tracer
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
 tp, err := newJaegerTraceProvider(ctx)
 if err != nil {
  return nil, err
 }

 otel.SetTracerProvider(tp)
 otel.SetTextMapPropagator(
  propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}),
 )
 return tp, nil
}

func getUser(c *gin.Context, id string) string {
 // 在需要時將 http.Request 中內置的 `context.Context` 對象傳遞給 OpenTelemetry API。
 // 可以通過 gin.Context.Request.Context() 獲取。
 _, span := tracer.Start(
  c.Request.Context()"getUser", trace.WithAttributes(attribute.String("id", id)),
 )
 defer span.End()

 // mock 業務邏輯
 if id == "7" {
  return "Q1mi"
 }
 return "unknown"
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zTtX4XeKPRsyltTS-J7N8g