news 2026/10/31 6:58:26

OpenTelemetry埋点实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenTelemetry埋点实战

一、环境准备

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:latest
  • 16686:Jaeger UI

  • 4317/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 :

  1. Service 下拉选hello-otel→ Find Traces;

  2. 看到链路瀑布图: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/otelgin
r := 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/metric
func 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/otelslog
logger := 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_ENDPOINTOTLP 地址,如http://otelcol:4317(SDK 会自动加 /v1/traces 等)
OTEL_EXPORTER_OTLP_PROTOCOLgrpc/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),通过环境变量差异化配置——这就是"平台工程"式的落地方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 6:56:58

2026 翻译工具盘点:26 款翻译软件与开源工具,按场景怎么选

精选翻译工具与工作流目录:涵盖随身出行、网页阅读、复杂文档排版、学术研读、开源本地引擎与开发者接入。 不同场景下的翻译任务有着截然不同的诉求:在手机上快速识别路牌与菜单,在浏览器中对照阅读技术文档,在保留版面结构的前提…

作者头像 李华
网站建设 2026/10/31 6:53:39

如何把 PDF 变成原生可编辑的 PPT?PPT Master 5 分钟跑通指南

如何把 PDF 变成原生可编辑的 PPT?PPT Master 5 分钟跑通指南 【免费下载链接】ppt-master AI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations, data-backed charts and tables on demand, audio …

作者头像 李华
网站建设 2026/10/31 6:49:06

uBlock Origin:免费内容拦截器,广告请求出网前就被拒收

uBlock Origin:免费内容拦截器,广告请求出网前就被拒收 【免费下载链接】uBlock uBlock Origin - An efficient blocker for Chromium and Firefox. Fast and lean. 项目地址: https://gitcode.com/GitHub_Trending/ub/uBlock 你打开一个新闻页&a…

作者头像 李华
网站建设 2026/10/31 6:43:08

第八篇:把每日推荐放到桌面:健康菜谱助手的元服务与服务卡片

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/31 6:41:11

CCS的使用、cmd文件、CCXML 文件、新建工程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华