Skip to content

Jaeger

Jaeger Github

部署 Jaeger

  • :16686 是 Jaeger 的 UI(网页界面)端口
  • :14268 是接收 Trace 数据的 HTTP 接口端口
sh
docker run -d --name jaeger \
	-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
	-p 16686:16686 \
	-p 14268:14268 \
	jaegertracing/all-in-one:latest

访问 http://localhost:16686 验证

第一个案例:在 API 服务中埋点

go
package main

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

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
	"go.opentelemetry.io/otel/trace"
)

func main() {
	shutdown := InitTracer("order-api")
	defer shutdown(context.Background())

	// 注册 handler,加埋点中间件
	//http.Handle("/query", otelhttp.NewHandler(http.HandlerFunc(HandleOrder), "QueryOrder"))
	http.Handle("/query", http.HandlerFunc(HandleOrder))

	fmt.Println("🚀 服务启动咯:http://localhost:8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatalf("启动失败:%v", err)
	}
}

func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		span := trace.SpanFromContext(ctx)
		traceID := span.SpanContext().TraceID().String()
		spanID := span.SpanContext().SpanID().String()

		log.Printf("[trace_id=%s][span_id=%s] 请求开始: %s %s", traceID, spanID, r.Method, r.URL.Path)

		next.ServeHTTP(w, r)
	})
}

func InitTracer(serviceName string) func(context.Context) error {
	// 配置同步采样器,确保每个 trace 都被采样
	//sampler := sdktrace.ParentBased(sdktrace.AlwaysSample())

	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
	if err != nil {
		log.Fatalf("failed to initialize exporter: %v", err)
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String(serviceName),
		)),

		//sdktrace.WithSampler(sampler),
		//sdktrace.WithSyncer(exp), // 使用同步提交方式
	)

	otel.SetTracerProvider(tp)
	return tp.Shutdown
}

func HandleOrder(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	tracer := otel.Tracer("order-handler")
	ctx, span := tracer.Start(ctx, "HandleOrder")
	defer span.End()

	orderID := r.URL.Query().Get("order_id")

	order := queryOrder(ctx, orderID)
	updateOrder(ctx, order)
	stock := getStockFromThirdParty(ctx, order.SKU)
	saveToDB(ctx, order, stock)
	writeToCache(ctx, order)

	traceID := span.SpanContext().TraceID().String() // 获取 TraceID

	w.Write([]byte(traceID))
}

type Order struct {
	ID  string
	SKU string
}

func queryOrder(ctx context.Context, id string) Order {
	ctx, span := otel.Tracer("order").Start(ctx, "queryOrder")
	defer span.End()

	// 模拟 DB 查询
	time.Sleep(100 * time.Millisecond)
	span.SetAttributes(attribute.String("order.id", id))
	return Order{ID: id, SKU: "sku-123"}
}

func updateOrder(ctx context.Context, order Order) {
	ctx, span := otel.Tracer("order").Start(ctx, "updateOrder")
	defer span.End()

	time.Sleep(50 * time.Millisecond)
}

func getStockFromThirdParty(ctx context.Context, sku string) int {
	client := http.Client{
		Transport: otelhttp.NewTransport(http.DefaultTransport),
	}

	req, _ := http.NewRequestWithContext(ctx, "GET", "http://stock-service/sku/"+sku, nil)
	res, err := client.Do(req)
	if err != nil {
		return 0
	}
	defer res.Body.Close()

	return 100
}

func saveToDB(ctx context.Context, order Order, stock int) {
	ctx, span := otel.Tracer("order").Start(ctx, "saveToDB")
	defer span.End()

	span.SetAttributes(
		attribute.Int("stock.left", stock),
		attribute.String("order.sku", order.SKU),
	)
}

func writeToCache(ctx context.Context, order Order) {
	ctx, span := otel.Tracer("order").Start(ctx, "writeToCache")
	defer span.End()

	// 这里你可以封装 redis 客户端,把 ctx 传进去以注入 trace
}

前后端集成链路追踪

要让 前端服务后端服务 在同一条链路上,核心就是 传递追踪信息(TraceIDSpanID。前端发起请求时,需要将其当前的链路信息传递给后端,后端再继承并继续该链路追踪。

下面是如何实现前后端服务共享同一条链路追踪的一个示例。

1. 前端服务(React/JavaScript)

前端服务的任务是获取当前的 TraceIDSpanID,然后通过 HTTP 请求头传递给后端。

(a) 前端发送请求时加入追踪信息

这里,我们假设前端使用了 opentelemetry-web 来生成链路信息。生成的追踪信息会以 traceparent 形式传递。

js
// 引入 OpenTelemetry 和必要的包
import {
  WebTracerProvider,
  SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-web";
import { ConsoleSpanExporter } from "@opentelemetry/tracing";
import { TraceIdRatioBasedSampler } from "@opentelemetry/core";

// 初始化 OpenTelemetry
const provider = new WebTracerProvider({
  sampler: new TraceIdRatioBasedSampler(1), // 100% 采样
});
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();

// 获取当前的 tracer
const tracer = provider.getTracer("frontend-tracer");

// 创建一个 Span(前端请求的起点)
const span = tracer.startSpan("frontend-request");

// 通过 traceparent 传递链路信息给后端
const traceparent = span.context().toTraceparent();

// 发起 HTTP 请求,并将 traceparent 放入请求头
fetch("http://your-backend-service/query", {
  method: "GET",
  headers: {
    traceparent: traceparent, // 将 traceparent 传递给后端
  },
})
  .then((response) => response.json())
  .then((data) => {
    console.log("Received data from backend:", data);
    span.end(); // 请求完成后结束 Span
  });

2. 后端服务(Golang)

后端服务需要从请求中提取前端传来的 traceparent 信息,并根据这些信息创建一个新的 Span,继续沿用同一条链路。

(a) 后端服务读取传递过来的链路信息并继续追踪

后端服务会使用 OpenTelemetry 的 otelhttp 组件来自动传递链路信息。

go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/instrumentation/net/http/otelhttp"
)

var tracer = otel.Tracer("backend-tracer")

// 后端处理函数
func HandleRequest(w http.ResponseWriter, r *http.Request) {
	// 从 HTTP 请求头中获取 traceparent 信息
	traceparent := r.Header.Get("traceparent")
	if traceparent == "" {
		log.Println("No traceparent found in request header, starting new trace")
		// 没有 traceparent,开始一个新的 Span
		ctx, span := tracer.Start(context.Background(), "backend-service")
		defer span.End()
	} else {
		// 从传递来的 traceparent 信息创建新的 Span
		ctx, span := tracer.Start(r.Context(), "backend-service", trace.WithSpanKind(trace.SpanKindServer))
		defer span.End()
		span.SetAttributes(
			trace.StringAttribute("traceparent", traceparent),
		)
	}

	// 后端服务执行逻辑
	fmt.Fprintf(w, "Hello from backend!")
}

func main() {
	http.HandleFunc("/query", HandleRequest)

	// 使用 otelhttp 包来传递链路信息
	http.ListenAndServe(":8080", otelhttp.NewHandler(http.DefaultServeMux, "server"))
}

3. 链路追踪的工作原理

  1. 前端:

    • 在前端,traceparent(包含 TraceIDSpanID)会被加入到 HTTP 请求头中,发送到后端。
  2. 后端:

    • 后端通过读取 HTTP 请求头中的 traceparent,获取当前的链路信息。
    • 后端根据 traceparent 生成自己的 Span,标记为 "backend-service"。
  3. 在 Jaeger 上看到的效果:

    • Jaeger 会将前端请求和后端请求的日志合并在同一条链路上,显示从前端到后端的整个流程。

4. 总结:

  • 前端: 需要使用 OpenTelemetry 来生成 TraceIDSpanID,并通过 HTTP 请求头(traceparent)将其传递给后端。
  • 后端: 后端从 HTTP 请求头读取 traceparent 信息,并根据这些信息继续创建新的 Span,确保链路追踪的一致性。
  • Jaeger 展示: 在 Jaeger 中,前端和后端请求会显示在同一条链路上,帮助你追踪跨服务的调用链。

通过这种方式,你的 前端服务后端服务 就可以共享同一条链路追踪,确保整个请求的流转在 Jaeger 中能够被完整展示!