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 以響應頭的方式返回給前端,可以添加以下中間件。
注意:
不能直接傳遞 gin 框架的
gin.Context
,需要傳遞 http.Request 中內置的context.Context
。響應頭中的 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