You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenTelemetry Go服务如何在调用RecordError时强制始终追踪Span

解决OpenTelemetry中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 22:19:08