基于OTel的HTTP链路追踪
承蒙大家厚爱,我的《Go语言之路》的纸质版图书已经上架京东,有需要的朋友请点击 此链接 购买。
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/propagation"
"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 链路追踪示例
// 上报 trace 数据至 Jaeger
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
}
// 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 main() {
ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}()
tr := otel.Tracer("http-client")
ctx, span := tr.Start(ctx, "GET BLOG", trace.WithAttributes(semconv.PeerService(peerServiceName)))
defer span.End()
// 创建一个 http client,带有链路追踪的配置
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// 构造请求
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, blogURL, nil)
// 发起请求
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
// 解析响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
fmt.Printf("body:%s\n", body)
}
trace 数据:
要深入net/http
内部的追踪可以使用net/http/httptrace
,会采集dns
、connect
、tls
等环节。
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/propagation"
"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"
)
// 使用 net/http/httptrace 深入 net/http 内部的链路追踪示例
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
}
// 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 main() {
ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
_ = tp.Shutdown(ctx)
}()
// 创建 http client,配置trace
client := http.Client{
Transport: otelhttp.NewTransport(
http.DefaultTransport,
otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
return otelhttptrace.NewClientTrace(ctx)
}),
),
}
// 创建tracer
tr := otel.Tracer("http-client")
// 开启 span,PeerService 指要连接的目标服务
ctx, span := tr.Start(ctx, "GET BLOG", trace.WithAttributes(semconv.PeerService(peerServiceName)))
defer span.End()
// 构建请求
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, blogURL, nil)
// 发送请求
res, _ := client.Do(req)
body, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
fmt.Printf("Response Received: %s\n", body)
}
trace 数据:
服务端
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框架Jaeger示例
我们通常做 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"
)
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.Fatalf("initTracer failed, err:%v\n", err)
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
log.Fatalf("shutting down tracer provider failed, err:%v\n", 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"
}
trace 数据: