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 数据: http client trace

要深入net/http内部的追踪可以使用net/http/httptrace,会采集dnsconnecttls等环节。

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 数据: httptrace

服务端

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 以响应头的方式返回给前端,可以添加以下中间件。

注意:

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

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 数据: gin-trace

其他常用库的OTel相关内容


扫码关注微信公众号