一、环境准备
go version # >= 1.22 docker --version # 用于跑 Jaeger mkdir otel-demo && cd otel-demo go mod init otel-demo核心依赖(一次装齐):
go get go.opentelemetry.io/otel \ go.opentelemetry.io/otel/trace \ go.opentelemetry.io/otel/sdk/trace \ go.opentelemetry.io/otel/sdk/resource \ go.opentelemetry.io/otel/exporters/stdout/stdouttrace \ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc二、第一个程序:Span 打印到控制台
用stdouttrace导出器,先不依赖任何外部组件,直观看到"Span 长什么样"。
main.go:
package main import ( "context" "fmt" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" // 版本号以当前仓库为准 ) // initTracer 初始化全局 TracerProvider;返回一个供 defer 用的清理函数 func initTracer(ctx context.Context) (func(context.Context) error, error) { // 1. 创建导出器:把 Span 以 JSON 打到标准输出 exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) if err != nil { return nil, err } // 2. 创建 TracerProvider tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithBatcher(exp), // 生产标配:批量导出 sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceName("hello-otel"), )), ) // 3. 设为全局 Provider(第三方埋点库都读全局的) otel.SetTracerProvider(tp) return tp.Shutdown, nil // Shutdown 会把队列里剩余的 Span 全部发完 } func main() { ctx := context.Background() shutdown, err := initTracer(ctx) if err != nil { panic(err) } defer func() { if err := shutdown(ctx); err != nil { // 退出前 flush,防止丢数据! fmt.Println("shutdown:", err) } }() tracer := otel.Tracer("otel-demo/main") // 参数是 instrumentation scope(埋点库名) // 根 Span ctx, root := tracer.Start(ctx, "handleRequest") defer root.End() // 子 Span:模拟一次数据库查询 ctx, child := tracer.Start(ctx, "db.query", trace.WithAttributes(attribute.String("db.system", "mysql"), attribute.String("db.statement", "SELECT * FROM users"))) child.AddEvent("connection.pool.hit") // 时间点事件 child.SetAttributes(attribute.Int("db.rows_returned", 3)) child.End() fmt.Println("done. 查看 stdout 的 JSON 输出") }补一个 import(上面trace.WithAttributes需要):
"go.opentelemetry.io/otel/trace"运行:
go run .输出一段 JSON——这就是 OTel SDK 视角的 Span 结构(traceId、spanId、parentSpanId、attributes……),和 06 章 OTLP proto 字段一一对应。建议认真读一遍这个 JSON,后面读源码/协议会非常顺。
三、导出到 Jaeger:看到第一条链路
3.1 启动 Jaeger
Jaeger 1.35+ 原生支持 OTLP,一行启动:
docker run --rm --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ jaegertracing/all-in-one:latest16686:Jaeger UI4317/4318:OTLP gRPC / HTTP 接收端口
3.2 换用 OTLP gRPC 导出器
把initTracer里的导出器换掉:
import ( "google.golang.org/grpc/credentials/insecure" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" ) func initTracer(ctx context.Context) (func(context.Context) error, error) { exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint("localhost:4317"), // 本地开发跳过 TLS;生产必须配置真实证书 otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()), ) if err != nil { return nil, err } tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithBatcher(exp), sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceName("hello-otel"), )), ) otel.SetTracerProvider(tp) return tp.Shutdown, nil }再次go run .,等几秒(批量导出有延迟),打开 http://localhost:16686 :
Service 下拉选
hello-otel→ Find Traces;看到链路瀑布图:
handleRequest下挂着db.query,点击可查看属性和事件。
🎉 到这里,你已经走通了SDK → OTLP → 后端的完整链路。
3.3 中间加一层 Collector(生产形态)
先起一个最简 Collector:
docker run --rm --name otelcol \ -p 4317:4317 -p 4318:4318 \ otel/opentelemetry-collector-contrib:latest \ --config=/dev/null \ echo应用把 OTLP 指向 Collector 的 4317,Collector 再转发给 Jaeger——应用从此与后端解耦。
四、手动埋点进阶
4.1 记录错误(标准姿势)
codes来自go.opentelemetry.io/otel/codes。
4.2 Start 时的常用选项
ctx, span := tracer.Start(ctx, "call-downstream", trace.WithSpanKind(trace.SpanKindClient), // 出站调用标 CLIENT trace.WithAttributes(attribute.String("peer.service", "order-svc")), trace.WithTimestamp(start), // 自定义起始时间(默认 now) )4.3 跨服务传播(手写一遍,理解 otelhttp 做了什么)
客户端:
func callAPI(ctx context.Context, url string) ([]byte, error) { ctx, span := otel.Tracer("client").Start(ctx, "GET "+url, trace.WithSpanKind(trace.SpanKindClient)) defer span.End() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) // 关键:把 ctx 里的 trace 状态注入请求头 otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) resp, err := http.DefaultClient.Do(req) ... }服务端:
func handler(w http.ResponseWriter, r *http.Request) { ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) ctx, span := otel.Tracer("server").Start(ctx, "GET /api", trace.WithSpanKind(trace.SpanKindServer)) defer span.End() // ... 用 ctx 处理,下游链路自动接上 }两段代码合起来,就是 traceparent 头的注入与提取(02 章概念落地)。第三方库(otelhttp/otelgin)做的事本质就是这两步 + 语义约定属性。
五、自动埋点:gin / http / grpc
手动埋点适合业务关键路径;中间件类框架代码用现成的库(都在opentelemetry-go-contrib仓库)。
5.1 gin
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/otelginr := gin.Default() r.Use(otelgin.Middleware("my-api")) // 每个请求自动生成 SERVER Span r.GET("/users/:id", func(c *gin.Context) { // 从请求 ctx 取出当前 Span,继续手动加子 Span / 属性 span := trace.SpanFromContext(c.Request.Context()) span.SetAttributes(attribute.String("user.id", c.Param("id"))) })5.2 net/http(服务端 + 客户端)
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp// 服务端:包一层 handler handler := otelhttp.NewHandler(myHandler, "api") // 客户端:包一层 Transport(出站自动 CLIENT Span + 注入 traceparent) client := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }5.3 gRPC
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc// 新版本推荐使用 stats handler 方式(拦截器方式已标记废弃) srv := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()), ) // 客户端 conn, err := grpc.NewClient(addr, grpc.WithStatsHandler(otelgrpc.NewClientHandler()), )5.4 数据库等
contrib 仓库还有go-orm、database/sql(通过otelsql包裹 driver)、redis、kafka等常见组件的埋点,用法都是在构造客户端时包一层。查组件清单:https://go.opentelemetry.io/contrib/instrumentation
六、加上 Metrics
以 OTLP 推送指标为例(和 trace 同一个端口):
go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc \ go.opentelemetry.io/otel/sdk/metricfunc initMeter(ctx context.Context) (func(context.Context) error, error) { exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithEndpoint("localhost:4317"), otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()), ) if err != nil { return nil, err } mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader( sdkmetric.NewPeriodicReader(exp, sdkmetric.WithInterval(10*time.Second)), // 定期推送 )) otel.SetMeterProvider(mp) return mp.Shutdown, nil }业务里使用:
var ( meter = otel.Meter("otel-demo") reqCount metric.Int64Counter reqDur metric.Float64Histogram ) func init() { reqCount, _ = meter.Int64Counter("app.request.count", metric.WithDescription("请求总数")) reqDur, _ = meter.Float64Histogram("app.request.duration", metric.WithUnit("s")) } func recordRequest(route string, dur float64, code int) { attrs := metric.WithAttributes( attribute.String("http.route", route), attribute.Int("http.response.status_code", code), ) reqCount.Add(context.Background(), 1, attrs) reqDur.Record(context.Background(), dur, attrs) }想Prometheus 抓取模式(pull)的话,把导出器换成
go.opentelemetry.io/otel/exporters/prometheus,暴露/metrics即可。
七、日志关联 trace
推荐用 slog + 桥接,三步:
go get go.opentelemetry.io/contrib/bridges/otelsloglogger := slog.New(otelslog.NewHandler("my-app", otelslog.WithLoggerProvider(otel.GetLoggerProvider()))) otel.SetLogger(logger) // 顺便让 OTel 自身日志走 slog // 使用:必须传 ctx! func handle(ctx context.Context) { ctx, span := otel.Tracer("x").Start(ctx, "handle") defer span.End() logger.InfoContext(ctx, "处理订单", slog.String("order_id", "A123")) // → 输出自动带 trace_id / span_id(Jaeger 里能按 trace 搜到这条日志) }最低成本方案(不加依赖)是 02 章的自定义traceHandler——按团队情况二选一。
八、常用环境变量
OTel 规范定义了标准环境变量,Go SDK 支持:
| 变量 | 作用 |
|---|---|
OTEL_SERVICE_NAME | 设置 resource 的 service.name |
OTEL_RESOURCE_ATTRIBUTES | 附加资源属性,如env=prod,team=trade |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP 地址,如http://otelcol:4317(SDK 会自动加 /v1/traces 等) |
OTEL_EXPORTER_OTLP_PROTOCOL | grpc/http/protobuf/http/json |
OTEL_TRACES_SAMPLER+OTEL_TRACES_SAMPLER_ARG | 如parentbased_traceidratio/0.1 |
OTEL_BSP_SCHEDULE_DELAY | 批量导出间隔(毫秒,默认 5000) |
OTEL_BSP_MAX_QUEUE_SIZE | 批处理队列上限(默认 2048) |
OTEL_BSP_MAX_EXPORT_BATCH_SIZE | 单批最大 Span 数(默认 512) |
OTEL_PROPAGATORS | 传播器,如tracecontext,baggage |
代码里的WithXxx选项优先级高于环境变量(显式配置覆盖环境)。
九、FAQ
Q1:Jaeger 里看不到链路?按顺序查:①shutdown是否被调用(Batcher 是异步批量的,不 shutdown 队列里数据会丢);② 导出器 endpoint 端口对不对(gRPC 4317 / HTTP 4318);③ docker 端口是否映射出来;④ 先换SimpleSpanProcessor(sdktrace.WithSyncer(exp))同步导出排除批量问题。
Q2:链路断了,下游 Span 的 parent 指向不存在?九成是 ctx 没传(新开context.Background())或出站请求没注入/入站没提取 propagator。跨服务断链重点查otelhttp/otelgrpc有没有把两端都包上。
Q3:Span 太多影响性能?① 降采样率;② 用sdktrace.WithSpanLimits限制属性/事件数量;③ 检查是否误用了WithSyncer(同步导出阻塞业务)。
Q4:属性里的中文/特殊字符?属性值是 UTF-8 字符串,没问题;但避免高基数取值(见 02 章 1.4)。
Q5:多个服务怎么共用一套初始化代码?把initTracer/initMeter抽成内部包(公司统一 starter),通过环境变量差异化配置——这就是"平台工程"式的落地方式。