基于openTelementary进行链路追踪,对于http handler和grpc服务,企业开发一般是如何追踪的,从添加span开始为我介绍,一直到最后的jaeger可视化呈现,以go语言为例
企业里用 OpenTelemetry 做 Go 服务的链路追踪,套路其实很固定:SDK 初始化 → 自动埋点(HTTP/gRPC)→ 手动补 Span → OTLP 导出 → Jaeger 可视化。下面按这条主线走一遍,每个环节都给出企业常用写法。
🧩 整体链路长什么样
一个请求从浏览器/客户端进来,经过网关 → HTTP 服务 A → gRPC → 服务 B,每一跳都应该自动续上同一个 TraceID。OpenTelemetry 在这套体系里的角色是:
- TracerProvider:管 Span 怎么创建、采样、导出
- Propagator:负责 TraceContext 跨进程传递(W3C Trace Context 是默认标准)
- Instrumentation 包:HTTP / gRPC 的自动埋点拦截器
- Exporter:把 Span 送到 Jaeger(现在推荐 OTLP,不再推荐 jaeger exporter)
第一步:初始化 TracerProvider(全局只做一次)
这一步是企业落地的地基,所有服务启动时都要先跑,配置一次挂到全局。
import (
"context"
"os"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"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.21.0"
)
func InitTracer() (*sdktrace.TracerProvider, error) {
// 1️⃣ 创建 OTLP gRPC Exporter,指向 Collector
client := otlptracegrpc.NewClient(
otlptracegrpc.WithInsecure(), // 生产换成 TLS
otlptracegrpc.WithEndpoint("otel-collector:4317"),
)
exporter, err := otlptracegrpc.New(context.Background(), client)
if err != nil {
return nil, err
}
// 2️⃣ 配置资源属性(Jaeger UI 里看到的 service.name 就来自这里)
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-service"), // 必须,否则 Jaeger UI 看不到服务名
semconv.ServiceVersion("1.0.0"),
semconv.DeploymentEnvironment(os.Getenv("ENV")),
),
)
if err != nil {
return nil, err
}
// 3️⃣ 创建 TracerProvider,配置批处理和采样率
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter, // 批量异步导出,减少 IO
sdktrace.WithMaxQueueSize(2048),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), // 10% 采样
)
// 4️⃣ 设置为全局 Provider,并注册 W3C Trace Context 传播器
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
⚠️ 关键踩坑点:
go.opentelemetry.io/otel/exporters/jaeger已废弃(2023 年 7 月起不再维护),官方推荐用 OTLP exporter 对接 Jaeger 的 OTLP Receiver。如果你在网上看到jaeger.New(jaeger.WithCollectorEndpoint(...))这种写法,那是老教程,别抄。service.name必须在 Resource 里声明,否则 Jaeger UI 里服务名是空的。- 采样率不要写死
AlwaysSample(),生产环境 10% 或更低才扛得住流量。
main.go 里调用方式:
func main() {
tp, err := InitTracer()
if err != nil {
panic(err)
}
defer tp.Shutdown(context.Background())
// 下面启动 HTTP / gRPC 服务
}
第二步:HTTP Handler 的自动埋点
企业里最常见的做法是用 otelhttp 的 middleware,一行代码包住原有 handler,零侵入。
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", createOrderHandler)
mux.HandleFunc("/healthz", healthzHandler)
// 关键一行:用 otelhttp.NewHandler 包住整个 mux
wrappedHandler := otelhttp.NewHandler(mux, "order-service")
http.ListenAndServe(":8080", wrappedHandler)
}
这一行帮你做了什么:
| 能力 | 自动完成 |
|---|---|
| 创建 Span | 每个请求自动建一个根 Span,span name 为 "order-service" |
| 注入 Context | r.Context() 里已经带了 span,业务代码直接用 |
| 记录属性 | http.method、http.route、http.status_code、http.host 自动打上 |
| 状态码推断 | 根据 HTTP 状态码自动设 span status(4xx/5xx 会标记异常) |
业务 handler 里如果想追加自定义 Span,直接从 context 拿 tracer 就行,父子关系自动串联:
func createOrderHandler(w http.ResponseWriter, r *http.Request) {
tracer := otel.GetTracerProvider().Tracer("")
ctx, span := tracer.Start(r.Context(), "createOrder")
defer span.End()
span.SetAttributes(
attribute.String("order.user_id", r.Header.Get("X-User-ID")),
attribute.String("order.type", "express"),
)
// 执行业务逻辑...
result, err := processOrder(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "order processing failed")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
第三步:HTTP Client 调用下游(保持链路不断)
A 服务调 B 服务时,必须把 TraceContext 注入到 HTTP 头里,否则 B 服务会新建一个 Trace,链路就断了。
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequest("GET", "http://payment-service/api/pay", nil)
// TraceContext 会自动写入 traceparent header
resp, err := client.Do(req)
otelhttp.NewTransport 做的事情就是:在发出请求前把当前 span 的 TraceID/SpanID 序列化成 traceparent 头,目标服务收到后自动解析并续上链路。
第四步:gRPC 服务的自动埋点
gRPC 这边用的是 otelgrpc 的拦截器,服务端和客户端各注册一下就行。
服务端:
import (
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func main() {
server := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
pb.RegisterOrderServiceServer(server, &OrderService{})
server.Serve(lis)
}
客户端:
conn, _ := grpc.Dial("localhost:50051",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
client := pb.NewPaymentServiceClient(conn)
resp, err := client.CreatePayment(ctx, &pb.CreatePaymentRequest{...})
拦截器会自动完成:提取 incoming metadata 里的 TraceContext → 创建 span → 注入到 outgoing 调用的 header 里。业务代码完全不用改。
如果你想在 gRPC handler 里加业务属性,同样从 ctx 取 span:
func (s *OrderService) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.String("order.id", req.OrderId),
attribute.Int64("order.amount", req.Amount),
)
// ...
}
第五步:Span 的生命周期管理(企业级要点)
手动创建 Span 有几个容易翻车的地方,单独说一下:
✅ 正确姿势:
func doWork(ctx context.Context) {
tracer := otel.GetTracerProvider().Tracer("")
ctx, span := tracer.Start(ctx, "doWork") // 用传入的 ctx,自动成子 span
defer span.End() // 一定记得 End,否则内存泄漏
span.SetAttributes(attribute.String("key", "value"))
}
❌ 常见错误:
- 用 **
context.Background()起新 span** → 链路断裂,变成独立 trace - **忘记 **
defer span.End()→ span 不关闭,Jaeger 上看不到 - 用 **
context.WithValue(ctx, "span", span)传 span** → 污染 context 树,正确做法是tracer.Start(ctx, ...)返回的 ctx 本身就带 span RecordError(err)** 忘了配 **SetStatus(codes.Error, ...)→ 错误不会标红
第六步:OpenTelemetry Collector 中转(强烈推荐)
企业里一般不推荐让每个服务直连 Jaeger,中间加一层 Collector 好处很多:
- 统一数据格式(OTLP)
- 采样率可以在 Collector 层动态调整
- 后端换 Jaeger / Tempo / 阿里云 ARMS 不用改业务代码
典型 otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger]
Go 服务那边 exporter 指向 Collector(otel-collector:4317),Collector 再转发给 Jaeger。
第七步:Jaeger UI 看链路
数据跑起来后,浏览器打开 http://localhost:16686:
- Service 下拉框选你的
service.name(比如order-service) - Operation 下拉框选具体接口(比如
POST /api/orders) - Time Range 选时间范围,点 Find Traces
- 点一条 trace,下方会出现甘特图,能看到完整的调用链:
┌─ order-service (HTTP) ──────────────── 150ms ──┐
│ ├─ createOrder │
│ │ ├─ gRPC → payment-service │
│ │ │ └─ CreatePayment 80ms │
│ │ └─ DB query 30ms │
└─────────────────────────────────────────────────┘
每个 span 都能展开看:
- Tags:
http.status_code、db.statement、order.id等业务属性 - Process:服务名、版本、部署环境
- Logs:span 内嵌的事件
- Errors:红色高亮,点开能看到堆栈
如果看不到数据,按这个顺序排查:
💡 排障清单:
- Exporter 是不是真的挂到了 TracerProvider?(漏掉
otel.SetTracerProvider(tp)是最常见原因)- Collector 端口通不通?(
telnet otel-collector 4317)- 采样率是不是 0?(临时改成
AlwaysSample()验证)- Jaeger 的 OTLP Receiver 开了没?
🏢 企业级最佳实践速查
| 维度 | 推荐配置 |
|---|---|
| Exporter | OTLP gRPC(弃用 jaeger exporter) |
| 采样率 | ParentBased(TraceIDRatioBased(0.1)),错误请求可强制采样 |
| service.name | 必须注入,用 semconv 常量 |
| 健康检查 | /healthz、/metrics 这类接口可跳过 tracing |
| 传播格式 | W3C TraceContext(默认) |
| 导出方式 | BatchSpanProcessor,队列 2048,批次 512 |
| Collector | 中间加一层,解耦业务和后端 |
这套组合拳下来,从 HTTP 入口 → gRPC 内部调用 → 数据库 → 下游服务,整条链路在 Jaeger 上一目了然,定位慢接口、排查跨服务故障都很顺手。