Appearance
Jaeger
部署 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
}前后端集成链路追踪
要让 前端服务 和 后端服务 在同一条链路上,核心就是 传递追踪信息(TraceID 和 SpanID)。前端发起请求时,需要将其当前的链路信息传递给后端,后端再继承并继续该链路追踪。
下面是如何实现前后端服务共享同一条链路追踪的一个示例。
1. 前端服务(React/JavaScript)
前端服务的任务是获取当前的 TraceID 和 SpanID,然后通过 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. 链路追踪的工作原理
前端:
- 在前端,
traceparent(包含TraceID和SpanID)会被加入到 HTTP 请求头中,发送到后端。
- 在前端,
后端:
- 后端通过读取 HTTP 请求头中的
traceparent,获取当前的链路信息。 - 后端根据
traceparent生成自己的 Span,标记为 "backend-service"。
- 后端通过读取 HTTP 请求头中的
在 Jaeger 上看到的效果:
- Jaeger 会将前端请求和后端请求的日志合并在同一条链路上,显示从前端到后端的整个流程。
4. 总结:
- 前端: 需要使用 OpenTelemetry 来生成
TraceID和SpanID,并通过 HTTP 请求头(traceparent)将其传递给后端。 - 后端: 后端从 HTTP 请求头读取
traceparent信息,并根据这些信息继续创建新的 Span,确保链路追踪的一致性。 - Jaeger 展示: 在 Jaeger 中,前端和后端请求会显示在同一条链路上,帮助你追踪跨服务的调用链。
通过这种方式,你的 前端服务 和 后端服务 就可以共享同一条链路追踪,确保整个请求的流转在 Jaeger 中能够被完整展示!