← 返回文章列表
使用指南 4 分钟阅读 炬鲸团队

Go 服务接入链路追踪:OpenTelemetry 手动埋点实战

自动埋点覆盖不全时,手动埋点是补全链路的关键。本文用 Go 演示 OpenTelemetry SDK 的初始化、Span 创建、属性与事件、错误标记和跨服务传播,附可直接运行的代码片段。

为什么需要手动埋点

自动埋点能覆盖 HTTP、数据库、消息队列等常见调用,但业务关键逻辑——一个循环、一次外部调用、一段加密计算——它看不到。手动埋点就是在这些位置显式创建 Span,把业务语义补进链路里。本文以 Go 为例,从头搭一个可用的埋点工程。

初始化 SDK

先引入依赖并初始化 TracerProvider,导出到 Collector:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
    exp, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }
    tp := trace.NewTracerProvider(
        trace.WithBatcher(exp),
        trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.1))),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

WithBatcher 批量上报,TraceIDRatioBased(0.1) 按 10% 采样。生产环境建议在 HTTP、数据库这类框架的入口已经由自动埋点覆盖的前提下,手动埋点只用来补充业务语义。

创建 Span 与属性

Span 是链路的基本单元。用 context 贯穿整个调用,父子关系自动建立:

func ProcessOrder(ctx context.Context, orderID string) error {
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "process_order")
    defer span.End()

    span.SetAttributes(
        attribute.String("order.id", orderID),
        attribute.String("service.version", "1.4.2"),
    )
    // 业务逻辑...
    return nil
}

属性要克制:只放需要检索的字段(订单号、用户 ID、版本号),不要塞整个请求体,否则存储和查询成本都会涨。事件用于记录时间点,比如"支付回调已收到":

span.AddEvent("payment_callback_received",
    trace.WithAttributes(attribute.String("channel", "wechat")))

错误标记

调用失败时,除了记日志,还要给 Span 打上错误状态,这样链路图里能一眼标红:

if err := callInventory(ctx); err != nil {
    span.RecordError(err)
    span.SetStatus(codes.Error, err.Error())
}

RecordError 把错误信息作为事件记录,SetStatus 把 Span 状态置为 ERROR。上层查询时按 status = ERROR 过滤,就能捞到所有失败链路。

跨服务传播

链路要不断,context 必须跟着请求走。HTTP 服务用中间件把 trace context 注入 header,下游用同一套 SDK 会自动接续:

otelhttp.NewHandler(handler, "order-api")

异步场景(goroutine、消息队列)要手动把 context 传进去,别在 goroutine 里用新的 context.Background(),否则父子关系就断了。采样的坑也在这里:父 Span 被采样了,子 Span 却做了新的采样决策,链路会缺一半。

验证

发几笔请求,到控制台按 trace id 查一条链路,确认每个 Span 的耗时、属性、错误状态都对。埋点不是写完就完事,链路完整性要靠一次真实请求来验证。