Spring 3中如何用Micrometer+OpenTelemetry在发送至Collector前过滤/丢弃Span?
在Spring 3结合Micrometer Tracing与OpenTelemetry时过滤Span的最优实现
问题描述
在Spring 3中使用Micrometer Tracing结合OpenTelemetry时,需要在Span发送至Collector前丢弃不需要的Span。尝试通过自定义OpenTelemetry Sampler实现过滤,但遇到以下问题:
- 当
management.tracing.sampling.probability=1.0时符合预期 - 当设置为
0.0时,父Span被丢弃,但子Span依然被发送,期望概率为0时不发送任何Span
原实现代码如下:
@Override public SamplingResult shouldSample(Context context, String traceId, String name, SpanKind spanKind, Attributes attributes, List<LinkData> list) { if (name.equalsIgnoreCase("xyz")) { return SamplingResult.drop(); } else { // 根据采样概率设置record或recordAndSample return SamplingResult.recordAndSample(); } } @Override public String getDescription() { return CustomSampler.class.getSimpleName(); }
@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class }) @EnableConfigurationProperties(TracingProperties.class) public class SamplerConfig { @Bean @Primary public Sampler customSampler() { Float samplerProbability = this.tracingProperties.getSampling().getProbability(); return Sampler.parentBasedBuilder(Sampler.traceIdRatioBased(samplerProbability)) .setRemoteParentNotSampled(new CustomSampler()) .setLocalParentNotSampled(new CustomSampler()) .setLocalParentSampled(new CustomSampler()) .setRemoteParentSampled(new CustomSampler()) .build(); } }
问题分析
原实现的核心问题在于:
- 自定义
CustomSampler在非目标Span名称的场景下直接返回SamplingResult.recordAndSample(),完全忽略了父Span的采样状态 ParentBasedBuilder的所有父Span分支都被替换为CustomSampler,导致当概率为0.0时,根Span被traceIdRatioBased(0.0)拒绝,但子Span的父Span未采样状态被CustomSampler覆盖,强制发送子Span
最优实现方案
方案1:正确的自定义Sampler(推荐,开销更低)
通过包装概率采样器实现先过滤Span名称,再继承父Span采样决策的逻辑,确保子Span会遵循父Span的采样状态:
自定义Sampler实现
public class CustomSampler implements Sampler { private final Sampler delegate; public CustomSampler(Sampler delegate) { this.delegate = delegate; } @Override public SamplingResult shouldSample(Context context, String traceId, String name, SpanKind spanKind, Attributes attributes, List<LinkData> links) { // 优先过滤指定名称的Span,直接丢弃 if (name.equalsIgnoreCase("xyz")) { return SamplingResult.drop(); } // 其余情况交给委托的概率采样器处理,自动继承父Span的采样决策 return delegate.shouldSample(context, traceId, name, spanKind, attributes, links); } @Override public String getDescription() { return "CustomSampler{" + delegate.getDescription() + "}"; } }
配置类优化
@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) @ConditionalOnClass({OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class}) @EnableConfigurationProperties(TracingProperties.class) public class SamplerConfig { private final TracingProperties tracingProperties; public SamplerConfig(TracingProperties tracingProperties) { this.tracingProperties = tracingProperties; } @Bean @Primary public Sampler customSampler() { Float samplerProbability = tracingProperties.getSampling().getProbability(); // 创建概率采样器 Sampler probabilitySampler = Sampler.traceIdRatioBased(samplerProbability); // 用自定义Sampler包装,实现名称过滤+概率采样的组合逻辑 Sampler filteredSampler = new CustomSampler(probabilitySampler); // 使用ParentBasedBuilder的默认配置,自动处理父Span的采样继承 return Sampler.parentBasedBuilder(filteredSampler).build(); } }
方案2:使用SpanProcessor(更灵活)
如果需要基于Span的更多属性(如持续时间、标签)过滤,可以使用SpanProcessor在Span结束时决定是否丢弃,适合复杂过滤场景:
@Bean public SpanProcessor filteringSpanProcessor() { return SpanProcessor.filtering(span -> { // 过滤条件:不是指定名称的Span,且Span已被采样 return !span.getName().equalsIgnoreCase("xyz") && span.getSpanContext().isSampled(); }); }
两种方案对比
- Sampler方案:在Span创建阶段就决定是否采样,能减少不必要的Span创建开销,同时影响上下文传播的采样标记,适合入口级过滤
- SpanProcessor方案:在Span结束阶段过滤,可基于更多运行时属性判断,灵活性更高,但已创建Span会产生少量额外开销
内容的提问来源于stack exchange,提问作者Anuj Singh
相关产品推荐
相关产品推荐

