Go 基础体系 · 第 90/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。
Go OpenTelemetry 实战:Trace、Metric、Log、Context 与 OTLP
本文以 Go 1.26.4 为基准,固定使用 otel v1.38.0、OTLP Exporter v1.38.0、otelhttp v0.63.0、otel/log 与 sdk/log v0.14.0 和 Collector Contrib 0.133.0。core、SDK、Exporter 与 contrib 应成组升级,禁止 latest。
OpenTelemetry(OTel)提供遥测 API、SDK、语义约定和 OTLP。SDK 负责采样、聚合与批量,Exporter 发送,Collector 处理;它不是存储系统。
1. 三类信号
Trace 描述请求的因果路径,Span 含时间、父子关系、属性、事件和状态。Metric 聚合数量、分布或当前值。Log 保存离散故障上下文。三者通过 resource、trace ID、span ID 和统一字段关联。
典型排障由指标发现异常,用 trace ID 找慢链路,再查相关日志。请求值不能做 metric label。
2. API、SDK 与 Provider
库代码用 OTel API 创建 tracer/meter,不安装 SDK 或 exporter;应用入口拥有 provider、processor、reader 和关闭。未配置 SDK 时 API 使用 no-op。
var tracer = otel.Tracer(
"example.com/article/internal/article",
trace.WithInstrumentationVersion("v1.8.0"),
)
func Publish(ctx context.Context, id string) error {
ctx, span := tracer.Start(ctx, "ArticleService.Publish")
defer span.End()
return publishToStore(ctx, id)
}
instrumentation scope 通常采用包路径,版本表示埋点库版本。全局 Provider 方便自动埋点;需要隔离的组件可注入 trace.TracerProvider。
3. Resource 标识产生遥测的实体
Resource 属性附在所有信号上,至少定义 service.name,并统一 namespace、instance、version 与 deployment environment:
resourceValue, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("article-api"),
semconv.ServiceVersion("1.8.0"),
attribute.String("deployment.environment.name", "production"),
),
)
if err != nil {
return fmt.Errorf("merge OpenTelemetry resource: %w", err)
}
Resource 在 provider 内不可变。升级 semconv 时检查字段和 schema URL,避免 dashboard 混用 key。
4. 初始化 OTLP Trace 管线
生产常通过 OTLP/gRPC 把数据发给本机或集群 Collector:
func newTracerProvider(ctx context.Context, res *resource.Resource) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector.observability.svc:4317"),
otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
})),
)
if err != nil {
return nil, fmt.Errorf("new OTLP trace exporter: %w", err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
sdktrace.WithBatcher(exporter,
sdktrace.WithMaxQueueSize(2048),
sdktrace.WithBatchTimeout(5*time.Second),
),
)
return provider, nil
}
生产应校验证书和服务身份。BatchSpanProcessor 用有界队列隔离请求与网络;队列满会丢 span,不能改成无界队列或让请求等待后端恢复。
5. Span 名称、属性、事件与状态
Span 名称应是低基数操作,如 ArticleService.Publish 或 GET /articles/{id},不能包含真实 ID。属性记录可聚合维度;事件描述 span 内某一时刻:
ctx, span := tracer.Start(ctx, "ArticleService.Publish",
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(attribute.String("article.state", "draft")),
)
defer span.End()
if err := repository.Publish(ctx, id); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "repository publish")
return fmt.Errorf("publish article %q: %w", id, err)
}
span.AddEvent("article published")
RecordError 不会自动设置 Error status。状态描述应稳定且不含秘密;404 是否为 Error 按协议统一约定。
6. Context 传播 Span 所有权
tracer.Start 返回含新 Span 的 context,必须传给下游;结束 Span 的函数拥有 defer span.End()。Context 是请求范围参数,不能保存在长期结构体。goroutine 若属于同一操作可继承 ctx,但必须响应取消并被等待;真正脱离请求的任务应建立独立有界生命周期和 link,而不是偷偷继续使用已取消 context。
Span 不是 error transport,业务函数仍返回 error,由边界记录一次。优先埋点网络、存储、队列和重要业务阶段,避免给纯内存小函数创建 span。
7. HTTP 自动埋点与路由名称
otelhttp v0.63.0 同时负责 W3C context 提取/注入和 HTTP span。服务端包装路由,客户端包装长期复用的 Transport:
handler := otelhttp.NewHandler(mux, "article-api",
otelhttp.WithSpanNameFormatter(func(_ string, request *http.Request) string {
return request.Method + " " + request.Pattern
}),
)
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
Timeout: 5 * time.Second,
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return err
}
response, err := client.Do(request)
路由模板如 /articles/{id} 是低基数,原始路径不是。中间件顺序要保证路由模板已确定,同时认证拒绝也能产生合适 span。响应 body 仍由调用方关闭;instrumentation 不接管业务资源所有权。
8. Propagation 与信任边界
默认跨进程传播 W3C traceparent、tracestate,需要行李时加 baggage:
propagator := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
otel.SetTextMapPropagator(propagator)
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(request.Header))
propagator.Inject(ctx, propagation.HeaderCarrier(outbound.Header))
远端父上下文可伪造,不是认证凭据。Baggage 不能放 token、个人信息或大对象;对外边界删除未允许 key 并限制 header 大小。
迁移 B3 时可组合 propagator,但应规定兼容期和唯一输出格式,避免长期注入多套 header。
9. 异步消息的传播与 Span Link
生产者注入消息 header,消费者提取:
carrier := propagation.MapCarrier(message.Headers)
otel.GetTextMapPropagator().Inject(ctx, carrier)
consumerCtx := otel.GetTextMapPropagator().Extract(context.Background(), carrier)
consumerCtx, span := tracer.Start(consumerCtx, "article.events process",
trace.WithSpanKind(trace.SpanKindConsumer),
)
defer span.End()
批量消费多个上游消息时使用 trace.WithLinks 关联多个 SpanContext 并限制数量。重试记录 attempt 和结果,不生成无限事件。
提取消息 header 失败时开始新 root,不能拒绝业务消息。trace 上下文也不能充当幂等键。
10. Metric Provider、Reader 与 Exporter
指标 SDK 通过 Instrument 记录测量,由 View 决定聚合,由 Reader 周期导出。OTLP push 示例:
exporter, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint("otel-collector.observability.svc:4317"),
otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)),
)
if err != nil {
return nil, fmt.Errorf("new OTLP metric exporter: %w", err)
}
reader := sdkmetric.NewPeriodicReader(exporter,
sdkmetric.WithInterval(30*time.Second),
sdkmetric.WithTimeout(10*time.Second),
)
provider := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(reader),
)
Prometheus 拉取模式使用另一 Reader,temporality 可能不同。Provider 属于进程;请求只使用 instrument 记录,不能重复注册同名指标。
11. Counter、Histogram、Gauge 与回调
Counter 记录单调增加数量,Histogram 记录延迟/大小分布,UpDownCounter 表示可增减在途量,Gauge 表示当前观测值:
meter := otel.Meter("example.com/article/internal/httpapi")
requests, err := meter.Int64Counter("article.http.server.requests")
if err != nil {
return err
}
duration, err := meter.Float64Histogram("article.http.server.duration",
metric.WithUnit("s"),
)
if err != nil {
return err
}
started := time.Now()
requests.Add(ctx, 1, metric.WithAttributes(attribute.String("http.route", route)))
duration.Record(ctx, time.Since(started).Seconds(),
metric.WithAttributes(attribute.String("http.route", route)),
)
单位使用 UCUM 约定,时间常用 s。异步 observable 回调可能被并发调用,不能做慢网络 I/O,也不能持有应用热锁;它只读取已维护的原子或受控快照。注册回调返回的 Registration 有生命周期,组件停止时应注销。
12. Cardinality 是指标设计的硬边界
每个属性组合都是时间序列。user_id、article_id、原始 URL、错误文本、SQL 和 request ID 不能作为 metric attribute;使用路由模板、状态码类别、稳定错误码和有限枚举。高基数 ID 仅进入受控 span 或日志。
SDK View 可删除属性、改聚合和 histogram 桶,Collector 也能过滤;但进入 SDK 后已消耗内存,最好在源头治理。关键指标应预估维度笛卡尔积。
Histogram 边界应对应 SLO;变更会影响历史可比性,应同步 dashboard 和告警。
13. Log 信号与 Trace 关联
Go 应用可通过 OTel Log Bridge 把 slog Record 转换为 OTel log record,或者继续输出结构化 stdout,由 Collector 的 file/stdout receiver 采集。无论路径如何,日志中加入当前 trace/span ID 才能可靠跳转:
func traceAttrs(ctx context.Context) []slog.Attr {
spanContext := trace.SpanContextFromContext(ctx)
if !spanContext.IsValid() {
return nil
}
return []slog.Attr{
slog.String("trace_id", spanContext.TraceID().String()),
slog.String("span_id", spanContext.SpanID().String()),
}
}
logger.LogAttrs(ctx, slog.LevelError, "publish article",
append(traceAttrs(ctx), slog.String("error_code", "repository_unavailable"))...,
)
日志版本线可能与 trace/metric 不同,升级前确认稳定级别。应用 OTLP 与 Collector 采 stdout 二选一,避免重复;日志和 span 不复制整个 payload。
14. Sampling:头采样、父采样与尾采样
AlwaysOn 适合低流量开发,TraceIDRatioBased 按 trace ID 比例稳定采样,ParentBased 遵循入口决策并让一条链保持完整。头采样在请求开始时决定,成本低,但尚不知道最终是否错误或缓慢。采样率 10% 不意味着每分钟每个小租户都精确 10%。
尾采样由 Collector 等待完整或超时的 trace 后按结果选择,可保留错误、慢请求和少量正常基线,但需要内存保存候选 spans,还要让同一 trace 的数据路由到同一决策点。尾采样前应用通常保持 AlwaysOn 或足够高的头采样,否则源头丢掉的数据无法恢复。
采样位会传播,但应防止不可信上游强制全采。记录接收、丢弃和决策延迟;指标不能按 trace 采样,日志采样须保留审计与严重错误。
15. Collector 的接收、处理和导出管线
Collector 集中处理 batching、内存保护、重试、属性治理和路由:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
batch:
send_batch_size: 8192
timeout: 5s
attributes/redact:
actions:
- key: http.request.header.authorization
action: delete
exporters:
otlp/backend:
endpoint: telemetry.example.com:4317
tls:
insecure: false
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, attributes/redact, batch]
exporters: [otlp/backend]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/backend]
logs:
receivers: [otlp]
processors: [memory_limiter, attributes/redact, batch]
exporters: [otlp/backend]
处理器顺序有语义:内存限制靠前,batch 靠后。配置用 0.133.0 镜像验证,并监控 Collector 队列、拒收、发送失败和内存。
16. Shutdown、Flush 与失败隔离
入口拥有 provider。先停流量并等待业务,再用新的有界 context 反向关闭:
func shutdownTelemetry(tp *sdktrace.TracerProvider, mp *sdkmetric.MeterProvider) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var errs []error
if err := tp.Shutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("shutdown tracer provider: %w", err))
}
if err := mp.Shutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("shutdown meter provider: %w", err))
}
return errors.Join(errs...)
}
ForceFlush 不应每请求调用。崩溃无法保证清空队列;普通观测通常 fail open 并告警,审计场景可按策略 fail closed。
17. 测试埋点而不依赖真实后端
SDK 内存 exporter/reader 可测试 span 和父子关系;传播可用 carrier 做 round trip:
func TestTraceContextPropagation(t *testing.T) {
propagator := propagation.TraceContext{}
spanContext := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{1},
SpanID: trace.SpanID{2},
TraceFlags: trace.FlagsSampled,
Remote: true,
})
ctx := trace.ContextWithRemoteSpanContext(context.Background(), spanContext)
carrier := propagation.MapCarrier{}
propagator.Inject(ctx, carrier)
got := trace.SpanContextFromContext(propagator.Extract(context.Background(), carrier))
if got.TraceID() != spanContext.TraceID() {
t.Fatalf("trace ID = %s, want %s", got.TraceID(), spanContext.TraceID())
}
}
CI 验证 Collector 配置并做 OTLP smoke test。测试必须关闭 provider,避免 batch goroutine 泄漏。
18. 性能与故障诊断
埋点成本来自属性分配、聚合、队列和序列化。用 benchmark/profile 测量;昂贵属性仅在 span.IsRecording() 时构造。
“没有 trace”依次检查 provider、propagator、采样、SDK drop、Exporter TLS/DNS、Collector receiver/processor/exporter 和查询时间。断链检查新 ctx、Transport 与消息 header;指标爆炸先找新增属性,再用 View 止血。
19. 安全、CI 与生产发布
遥测可能含 query、SQL、header 和用户数据。源头按允许列表采集,Collector 再删除;OTLP 使用 TLS/mTLS、最小权限和网络策略,管理端口不对公网开放。
CI 固定所有模块和 Collector 镜像,运行:
go test ./...
go test -race ./...
go vet ./...
go mod verify
otelcol-contrib validate --config=./deploy/otel-collector.yaml
发布先在 canary 比较延迟、资源、三类信号发送率、拒绝和费用。可用系统应能跨服务关联,采样和基数有预算,Collector 故障不拖垮业务,且敏感数据不离开信任边界。
系列导航与关联阅读
- 系列入口:Go 完整技术体系学习路线:从语法、并发到框架、中间件与 AI
- 上一篇:Go 开发工具链:Delve、Air、golangci-lint、govulncheck 与生成器
- 下一篇:Go Prometheus 与 Grafana:指标设计、埋点和告警
- 延伸:Go context 完整指南:取消、超时、Deadline 与 Value
- 延伸:Go gRPC 与 Protobuf 完整基础:IDL、Unary、Stream 与拦截器
- 延伸:Go Resty HTTP 客户端:请求封装、重试、认证与可观测性
官方资料
本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论