OpenTelemetry Go服务如何在调用RecordError时强制始终追踪Span
这问题我之前在Go项目里踩过坑,确实Sampler接口没法直接拿到Span后续添加的Error事件——毕竟采样决策是在Span创建时就敲定的,而RecordError是事后追加的操作。给你几个实用的落地方案,按侵入性和实现难度排序:
方案一:封装RecordError,强制修改已有Span的采样状态
这是最直接的方式,不需要提前预判错误,只需要替换业务里的RecordError调用为自己的封装函数。核心思路是利用Go的类型断言,访问OTel Span内部的采样决策修改方法(虽然是非公开接口,但主流OTel实现都支持):
import ( "context" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) // 封装自己的RecordError函数 func MyRecordError(ctx context.Context, err error, opts ...trace.EventOption) { span := trace.SpanFromContext(ctx) if !span.IsRecording() { // 先设置错误状态,再强制修改采样决策 span.SetStatus(codes.Error, err.Error()) // 类型断言调用内部方法,强制标记为需要采样导出 if s, ok := span.(interface{ SetSamplingDecision(trace.SamplingDecision) }); ok { s.SetSamplingDecision(trace.RecordAndSample) } } // 调用原始的RecordError记录错误事件 span.RecordError(err, opts...) }
优点:侵入性低,只需要替换RecordError的调用;不需要提前做任何预判。
注意:依赖OTel Span实现的内部方法,虽然稳定,但如果后续OTel版本变更可能需要调整。
方案二:创建强制采样的错误子Span
如果不想依赖内部接口,可以在记录错误时,创建一个强制采样的子Span,把错误信息放在这个子Span里。这样不管父Span有没有被采样,错误相关的链路一定会被收集:
func MyRecordError(ctx context.Context, err error, opts ...trace.EventOption) { // 创建一个AlwaysSample的子Span newCtx, errSpan := trace.NewSpan(ctx, "error.record", trace.WithSampler(trace.AlwaysSample())) defer errSpan.End() // 标记错误状态并记录事件 errSpan.SetStatus(codes.Error, err.Error()) errSpan.RecordError(err, opts...) // 复制父Span的属性,保证链路上下文完整 if parentSpan := trace.SpanFromContext(ctx); parentSpan.IsRecording() { errSpan.SetAttributes(parentSpan.Attributes()...) } }
优点:完全基于公开接口实现,兼容性好;错误链路独立,便于后续筛选。
缺点:会多生成一个子Span,增加少量链路数据量。
方案三:上下文标记+自定义采样器
如果业务里能提前预判可能出错的场景(比如进入某个风险函数),可以在上下文里设置强制采样标记,然后让自定义采样器识别这个标记:
第一步:定义上下文标记工具
import "context" var ctxKeyForceSample = struct{}{} // 给上下文添加强制采样标记 func WithForceSample(ctx context.Context) context.Context { return context.WithValue(ctx, ctxKeyForceSample, true) }
第二步:实现自定义采样器
type ForceSampleOnErrorSampler struct { delegate trace.Sampler // 原来的比例采样器 } func (s *ForceSampleOnErrorSampler) ShouldSample(p trace.SamplingParameters) trace.SamplingResult { // 检查上下文是否有强制采样标记 if forceSample, ok := p.Context.Value(ctxKeyForceSample).(bool); ok && forceSample { return trace.SamplingResult{Decision: trace.RecordAndSample} } // 没有标记则用原来的比例采样逻辑 return s.delegate.ShouldSample(p) }
第三步:业务中提前标记上下文
// 在可能出错的函数入口添加标记 func RiskyOperation(ctx context.Context) error { ctx = WithForceSample(ctx) // 创建Span时会触发采样器,强制采样 _, span := trace.NewSpan(ctx, "risky.operation") defer span.End() // ...业务逻辑... err := someRiskyFunc() if err != nil { span.RecordError(err) return err } return nil }
优点:采样决策在Span创建时完成,符合OTel的设计逻辑;不需要修改RecordError逻辑。
缺点:需要提前预判错误场景,侵入性较高。
方案四:自定义SpanProcessor,事后筛选导出错误Span
如果不想修改任何业务代码,可以通过SpanProcessor来拦截所有Span,在Span结束时检查是否包含错误,再决定是否强制导出:
第一步:实现自定义Processor
type ErrorExportProcessor struct { next trace.SpanProcessor // 后续的处理器(比如批量导出处理器) ratioSampler trace.Sampler // 原来的比例采样器 } func NewErrorExportProcessor(next trace.SpanProcessor, ratio float64) *ErrorExportProcessor { return &ErrorExportProcessor{ next: next, ratioSampler: trace.TraceIDRatioBased(ratio), } } func (p *ErrorExportProcessor) OnStart(parent context.Context, s trace.ReadWriteSpan) {} func (p *ErrorExportProcessor) OnEnd(s trace.ReadOnlySpan) { // 检查Span是否包含错误(状态码为Error或有error事件) hasError := s.Status().Code == codes.Error if !hasError { for _, event := range s.Events() { if event.Name == "error" { hasError = true break } } } if hasError { // 强制导出错误Span p.next.OnEnd(s) } else { // 非错误Span用原来的比例采样决定是否导出 result := p.ratioSampler.ShouldSample(trace.SamplingParameters{ TraceID: s.TraceID(), SpanID: s.SpanID(), }) if result.Decision == trace.RecordAndSample { p.next.OnEnd(s) } } } func (p *ErrorExportProcessor) Shutdown(ctx context.Context) error { return p.next.Shutdown(ctx) } func (p *ErrorExportProcessor) ForceFlush(ctx context.Context) error { return p.next.ForceFlush(ctx) }
第二步:初始化TracerProvider时配置
import "go.opentelemetry.io/otel/sdk/trace" // 假设已经初始化了exporter(比如Jaeger、OTLP exporter) tp := trace.NewTracerProvider( trace.WithSampler(trace.AlwaysSample()), // 先记录所有Span,后续由Processor决定是否导出 trace.WithSpanProcessor(NewErrorExportProcessor( trace.NewBatchSpanProcessor(exporter), 0.1, // 原来的比例采样率 )), )
优点:完全无业务侵入,只需要修改OTel配置。
缺点:会记录所有Span,高QPS场景下可能增加内存占用(因为不导出的Span也要先记录下来)。
内容的提问来源于stack exchange,提问作者abergmeier

