如何在 Go 语言中添加手动埋点
自动埋点追踪的是应用调用的第三方库,手动埋点追踪的则是你自己的代码。可以用它来统计结账步骤的耗时、给 span 挂上订单 ID,或者在记录失败时附上说明问题的上下文。
使用自托管 SigNoz?本页步骤对两种部署方式完全相同,区别只在于 OTLP endpoint 和 ingestion key,详见 Cloud to Self-Hosted。
前提条件
- 应用中必须已经运行 tracer provider 和 exporter。如果你使用 SDK,请先完成 OpenTelemetry Go instrumentation guide 中的核心配置。
- 在 handler 和业务逻辑中传递
context.Context。手动创建的 span 会从这些 context 派生,从而保持 trace 连贯。 - 已在 Go 1.25.1 和 OpenTelemetry Go SDK v1.38.0 上测试通过。
手动 span 可以与之共存。使用 compile-time instrumentation 时,注入的 SDK 会自动采集你在本页创建的 span;使用 eBPF agent 时,设置 OTEL_GO_AUTO_GLOBAL=true 即可让 agent 一并导出这些 span。
添加手动埋点
第 1 步:创建手动 span
使用 SDK tracer,把重要的业务逻辑包在自定义 span 里:
manual-span.goimport (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
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("order.status", "processing"),
)
// Use ctx for downstream work so child spans stay linked.
return nil}
几点建议:
- 复用 tracer 实例,不要每个请求都新建一个。
- 用与业务步骤对应的描述性名称启动 span(如
checkout、fetch-user等)。 - 确保每个 span 都被结束,
defer span.End()是最稳妥的做法。
步骤 2. 传播上下文
Go 不保存任何环境级别的追踪状态。Span 只能通过你传递给它的 context.Context 找到父级,因此 Go 比 Java 或 Python 更容易导致追踪链路断裂。请参见 Context Propagation 了解此步骤背后的概念。
进程内部
将 tracer.Start 返回的 ctx 传递给所有希望嵌套在该 span 下的函数:
func processOrder(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
// process-order 的子级。
validateOrder(ctx, orderID)
// 全新追踪的根 span。到 process-order 的链接已丢失。
validateOrder(context.Background(), orderID)
return nil
}
如果 goroutine 的生命周期长于请求,你需要故意派生一个新的上下文。一旦 handler 返回,请求上下文就会被取消,因此继续使用该上下文的后台任务将基于一个已取消的上下文开始。context.WithoutCancel 保留上下文中的值(包括 span),同时移除取消信号和截止时间:
// 保留追踪上下文,丢弃请求的取消信号。适用于 Go 1.21 及更高版本。
bgCtx := context.WithoutCancel(ctx)
go func() {
_, span := tracer.Start(bgCtx, "settle-order")
defer span.End()
// 在此处执行后台任务。
}()
跨越服务边界
otelhttp 等插桩库会自动为你完成注入和提取。对于它们未覆盖的环节,请自行编写调用。
在两端设置 span 类型。SigNoz 读取 Client 和 Server 来构建服务地图和 APM 指标。
在出站时,将上下文注入到 outgoing 请求头中:
inject.goimport (
"context"
"io"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
func chargeCard(ctx context.Context, body io.Reader) error {
ctx, span := tracer.Start(ctx, "charge-card", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://payments.internal/charge", body)
if err != nil {
return err
}
// 将 traceparent 头写入 req。
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 排空响应体以便复用连接。
_, err = io.Copy(io.Discard, resp.Body)
return err
}
在创建入口 span 之前,先进行提取(extract):
extract.gofunc handleCharge(w http.ResponseWriter, r *http.Request) {
// 若存在 traceparent 头,返回携带调用方 span 的 context;否则原样返回 r.Context()
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
ctx, span := tracer.Start(ctx, "handle-charge", trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
// 此时 handle-charge 成为调用方 trace 中 charge-card 的子 span。
capturePayment(ctx)
}
对于非 http.Header 的载体(例如队列消息元数据),请使用 propagation.MapCarrier:
// 发布者
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, carrier)
msg.Metadata = carrier // map[string]string
// 消费者
ctx := otel.GetTextMapPropagator().Extract(context.Background(), propagation.MapCarrier(msg.Metadata))
InfoInject 和 Extract 只有在通过 otel.SetTextMapPropagator 注册了 propagator 之后才会生效。Go instrumentation 指南在 initTracer 中配置了 propagation.TraceContext{} 和 propagation.Baggage{}。
步骤 3. 添加属性和事件
属性在 SigNoz 中以键值对形式呈现,方便筛选和聚合 spans。事件则用于记录 span 内的关键时刻。
attributes.goimport (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func handlePayment(ctx context.Context, amount float64) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.Float64("payment.amount", amount),
attribute.String("payment.currency", "USD"),
attribute.String("payment.method", "credit_card"),
)
span.AddEvent("payment.processed", trace.WithAttributes(
attribute.String("status", "success"),
attribute.String("transaction.id", "txn_123456"),
))
}
- 属性键保持一致(尽量遵循语义约定)。
- 用事件标记重试、缓存命中/未命中、队列等待等关键节点。
第 4 步:记录错误
在 span 上标记失败,方便在 SigNoz 中查询。
error-handling.goimport (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func riskyOperation(ctx context.Context) error {
span := trace.SpanFromContext(ctx)
err := doSomethingRisky()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
RecordError会附带堆栈和错误信息。- 将状态设为
codes.Error后,span 会出现在 SigNoz 的错误视图和告警中。 - 继续向上游传播错误,让调用方能够做出响应。
验证
- 触发会生成手动 span 的代码路径。
- 在 SigNoz 的 Traces 中按
service.name或 span 名称过滤。 - 打开某条 trace,检查属性、事件和错误状态。
- 查看 Errors 视图,确认失败记录中包含已记录的异常。
故障排查
在 SigNoz 中仍然看不到数据?请参阅排查缺失的 traces、日志和指标,其中涵盖 SDK 诊断、Collector 连接问题以及三类信号常见的数据接入错误。
为什么在 SigNoz 中看不到自定义 span?
- 确认
initTracer(以及自动埋点)在应用 handler 之前运行。 - 检查采样器配置。在开发环境使用
TraceIDRatioBased(0.01)可能会丢弃大部分手动 span。 - 确认请求流量确实打到了你插入 span 的函数上。
为什么明明创建了子 span,但它们却缺失了?
- 确保将
tracer.Start(或trace.SpanFromContext)返回的ctx传递给下游函数。 - HTTP/数据库客户端调用也必须接收传播的 context,否则链路追踪会在跨服务边界时断裂。
为什么下游服务总是自己开启新的 trace?
- 检查调用方是否对出站请求执行了
Inject,并确认请求中携带了traceparent。 - 检查接收方是否执行了
Extract,并将返回的 context 传入tracer.Start,而不是r.Context()或context.Background()。 - 确保两个服务注册了相同的 propagator 格式。参见 上下文传播。
为什么 span 上看不到 attributes 或 events?
- 属性键必须是字符串,且值必须使用正确的辅助方法(如
attribute.String、attribute.Float64等)。 - 在
span.End()之前调用span.AddEvent或span.SetAttributes。span 结束后的修改会被忽略。 - 避免复用已结束的 span。每次调用都应创建新的 span。
后续步骤
- 使用主流日志库从你的 Go 应用发送日志:Logrus、Zap 或 Zerolog
- 将 trace 与日志关联,以加速跨信号定位问题
- 通过 Go 插桩库目录 为常见 Go 框架添加插桩
- 阅读 上下文传播,了解跨语言通用的 wire format 及故障模式
获取帮助
如需本文操作步骤方面的帮助,请通过SigNoz 社区 Slack联系我们。如果你是 SigNoz Cloud 用户,可使用 SigNoz 实例右下角的产品内聊天支持,或邮件联系cloud-support@signoz.io。