Add option to export unsampled spans from span processors (#6057)

Co-authored-by: jack-berg <34418638+jack-berg@users.noreply.github.com>
This commit is contained in:
HaloFour 2024-01-04 15:33:30 -05:00 committed by GitHub
parent f4b5bbe829
commit 07351a2e9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 183 additions and 57 deletions

View File

@ -1,2 +1,12 @@
Comparing source compatibility of against Comparing source compatibility of against
No changes. *** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder setExportUnsampledSpans(boolean)
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.SimpleSpanProcessor (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder builder(io.opentelemetry.sdk.trace.export.SpanExporter)
+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder (not serializable)
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessor build()
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder setExportUnsampledSpans(boolean)

View File

@ -415,7 +415,7 @@ class OpenTelemetrySdkTest {
+ "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, " + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
+ "spanLimitsSupplier=SpanLimitsValue{maxNumberOfAttributes=128, maxNumberOfEvents=128, maxNumberOfLinks=128, maxNumberOfAttributesPerEvent=128, maxNumberOfAttributesPerLink=128, maxAttributeValueLength=2147483647}, " + "spanLimitsSupplier=SpanLimitsValue{maxNumberOfAttributes=128, maxNumberOfEvents=128, maxNumberOfLinks=128, maxNumberOfAttributesPerEvent=128, maxNumberOfAttributesPerLink=128, maxAttributeValueLength=2147483647}, "
+ "sampler=ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}, " + "sampler=ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}, "
+ "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}}" + "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}, exportUnsampledSpans=false}"
+ "}, " + "}, "
+ "meterProvider=SdkMeterProvider{" + "meterProvider=SdkMeterProvider{"
+ "clock=SystemClock{}, " + "clock=SystemClock{}, "

View File

@ -53,6 +53,7 @@ public final class BatchSpanProcessor implements SpanProcessor {
AttributeKey.booleanKey("dropped"); AttributeKey.booleanKey("dropped");
private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName(); private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName();
private final boolean exportUnsampledSpans;
private final Worker worker; private final Worker worker;
private final AtomicBoolean isShutdown = new AtomicBoolean(false); private final AtomicBoolean isShutdown = new AtomicBoolean(false);
@ -69,11 +70,13 @@ public final class BatchSpanProcessor implements SpanProcessor {
BatchSpanProcessor( BatchSpanProcessor(
SpanExporter spanExporter, SpanExporter spanExporter,
boolean exportUnsampledSpans,
MeterProvider meterProvider, MeterProvider meterProvider,
long scheduleDelayNanos, long scheduleDelayNanos,
int maxQueueSize, int maxQueueSize,
int maxExportBatchSize, int maxExportBatchSize,
long exporterTimeoutNanos) { long exporterTimeoutNanos) {
this.exportUnsampledSpans = exportUnsampledSpans;
this.worker = this.worker =
new Worker( new Worker(
spanExporter, spanExporter,
@ -96,10 +99,9 @@ public final class BatchSpanProcessor implements SpanProcessor {
@Override @Override
public void onEnd(ReadableSpan span) { public void onEnd(ReadableSpan span) {
if (span == null || !span.getSpanContext().isSampled()) { if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
return; worker.addSpan(span);
} }
worker.addSpan(span);
} }
@Override @Override
@ -135,6 +137,8 @@ public final class BatchSpanProcessor implements SpanProcessor {
return "BatchSpanProcessor{" return "BatchSpanProcessor{"
+ "spanExporter=" + "spanExporter="
+ worker.spanExporter + worker.spanExporter
+ ", exportUnsampledSpans="
+ exportUnsampledSpans
+ ", scheduleDelayNanos=" + ", scheduleDelayNanos="
+ worker.scheduleDelayNanos + worker.scheduleDelayNanos
+ ", maxExportBatchSize=" + ", maxExportBatchSize="

View File

@ -25,6 +25,7 @@ public final class BatchSpanProcessorBuilder {
static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000; static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000;
private final SpanExporter spanExporter; private final SpanExporter spanExporter;
private boolean exportUnsampledSpans;
private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS); private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS);
private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE; private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE;
@ -35,6 +36,15 @@ public final class BatchSpanProcessorBuilder {
this.spanExporter = requireNonNull(spanExporter, "spanExporter"); this.spanExporter = requireNonNull(spanExporter, "spanExporter");
} }
/**
* Sets whether unsampled spans should be exported. If unset, defaults to exporting only sampled
* spans.
*/
public BatchSpanProcessorBuilder setExportUnsampledSpans(boolean exportUnsampledSpans) {
this.exportUnsampledSpans = exportUnsampledSpans;
return this;
}
/** /**
* Sets the delay interval between two consecutive exports. If unset, defaults to {@value * Sets the delay interval between two consecutive exports. If unset, defaults to {@value
* DEFAULT_SCHEDULE_DELAY_MILLIS}ms. * DEFAULT_SCHEDULE_DELAY_MILLIS}ms.
@ -146,6 +156,7 @@ public final class BatchSpanProcessorBuilder {
public BatchSpanProcessor build() { public BatchSpanProcessor build() {
return new BatchSpanProcessor( return new BatchSpanProcessor(
spanExporter, spanExporter,
exportUnsampledSpans,
meterProvider, meterProvider,
scheduleDelayNanos, scheduleDelayNanos,
maxQueueSize, maxQueueSize,

View File

@ -36,7 +36,7 @@ public final class SimpleSpanProcessor implements SpanProcessor {
private static final Logger logger = Logger.getLogger(SimpleSpanProcessor.class.getName()); private static final Logger logger = Logger.getLogger(SimpleSpanProcessor.class.getName());
private final SpanExporter spanExporter; private final SpanExporter spanExporter;
private final boolean sampled; private final boolean exportUnsampledSpans;
private final Set<CompletableResultCode> pendingExports = private final Set<CompletableResultCode> pendingExports =
Collections.newSetFromMap(new ConcurrentHashMap<>()); Collections.newSetFromMap(new ConcurrentHashMap<>());
private final AtomicBoolean isShutdown = new AtomicBoolean(false); private final AtomicBoolean isShutdown = new AtomicBoolean(false);
@ -53,12 +53,17 @@ public final class SimpleSpanProcessor implements SpanProcessor {
*/ */
public static SpanProcessor create(SpanExporter exporter) { public static SpanProcessor create(SpanExporter exporter) {
requireNonNull(exporter, "exporter"); requireNonNull(exporter, "exporter");
return new SimpleSpanProcessor(exporter, /* sampled= */ true); return builder(exporter).build();
} }
SimpleSpanProcessor(SpanExporter spanExporter, boolean sampled) { public static SimpleSpanProcessorBuilder builder(SpanExporter exporter) {
requireNonNull(exporter, "exporter");
return new SimpleSpanProcessorBuilder(exporter);
}
SimpleSpanProcessor(SpanExporter spanExporter, boolean exportUnsampledSpans) {
this.spanExporter = requireNonNull(spanExporter, "spanExporter"); this.spanExporter = requireNonNull(spanExporter, "spanExporter");
this.sampled = sampled; this.exportUnsampledSpans = exportUnsampledSpans;
} }
@Override @Override
@ -73,22 +78,21 @@ public final class SimpleSpanProcessor implements SpanProcessor {
@Override @Override
public void onEnd(ReadableSpan span) { public void onEnd(ReadableSpan span) {
if (sampled && !span.getSpanContext().isSampled()) { if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
return; try {
} List<SpanData> spans = Collections.singletonList(span.toSpanData());
try { CompletableResultCode result = spanExporter.export(spans);
List<SpanData> spans = Collections.singletonList(span.toSpanData()); pendingExports.add(result);
CompletableResultCode result = spanExporter.export(spans); result.whenComplete(
pendingExports.add(result); () -> {
result.whenComplete( pendingExports.remove(result);
() -> { if (!result.isSuccess()) {
pendingExports.remove(result); logger.log(Level.FINE, "Exporter failed");
if (!result.isSuccess()) { }
logger.log(Level.FINE, "Exporter failed"); });
} } catch (RuntimeException e) {
}); logger.log(Level.WARNING, "Exporter threw an Exception", e);
} catch (RuntimeException e) { }
logger.log(Level.WARNING, "Exporter threw an Exception", e);
} }
} }
@ -128,6 +132,11 @@ public final class SimpleSpanProcessor implements SpanProcessor {
@Override @Override
public String toString() { public String toString() {
return "SimpleSpanProcessor{" + "spanExporter=" + spanExporter + '}'; return "SimpleSpanProcessor{"
+ "spanExporter="
+ spanExporter
+ ", exportUnsampledSpans="
+ exportUnsampledSpans
+ '}';
} }
} }

View File

@ -0,0 +1,36 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.trace.export;
import static java.util.Objects.requireNonNull;
/** Builder class for {@link SimpleSpanProcessor}. */
public final class SimpleSpanProcessorBuilder {
private final SpanExporter spanExporter;
private boolean exportUnsampledSpans;
SimpleSpanProcessorBuilder(SpanExporter spanExporter) {
this.spanExporter = requireNonNull(spanExporter, "spanExporter");
}
/**
* Sets whether unsampled spans should be exported. If unset, defaults to exporting only sampled
* spans.
*/
public SimpleSpanProcessorBuilder setExportUnsampledSpans(boolean exportUnsampledSpans) {
this.exportUnsampledSpans = exportUnsampledSpans;
return this;
}
/**
* Returns a new {@link SimpleSpanProcessor} with the configuration of this builder.
*
* @return a new {@link SimpleSpanProcessor}.
*/
public SimpleSpanProcessor build() {
return new SimpleSpanProcessor(spanExporter, exportUnsampledSpans);
}
}

View File

@ -517,6 +517,38 @@ class BatchSpanProcessorTest {
assertThat(exported).containsExactly(span.toSpanData()); assertThat(exported).containsExactly(span.toSpanData());
} }
@Test
void exportUnsampledSpans_recordOnly() {
WaitingSpanExporter waitingSpanExporter =
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
.thenReturn(SamplingResult.recordOnly());
sdkTracerProvider =
SdkTracerProvider.builder()
.addSpanProcessor(
BatchSpanProcessor.builder(waitingSpanExporter)
.setExportUnsampledSpans(true)
.setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build())
.setSampler(mockSampler)
.build();
ReadableSpan span1 = createEndedSpan(SPAN_NAME_1);
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
.thenReturn(SamplingResult.recordAndSample());
ReadableSpan span2 = createEndedSpan(SPAN_NAME_2);
// Spans are recorded and exported in the same order as they are ended, we test that a non
// exported span is not exported by creating and ending a sampled span after a non sampled span
// and checking that the first exported span is the sampled span (the non sampled did not get
// exported).
List<SpanData> exported = waitingSpanExporter.waitForExport();
// Need to check this because otherwise the variable span1 is unused, other option is to not
// have a span1 variable.
assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData());
}
@Test @Test
@Timeout(10) @Timeout(10)
@SuppressLogger(SdkTracerProvider.class) @SuppressLogger(SdkTracerProvider.class)
@ -569,6 +601,7 @@ class BatchSpanProcessorTest {
.hasToString( .hasToString(
"BatchSpanProcessor{" "BatchSpanProcessor{"
+ "spanExporter=mockSpanExporter, " + "spanExporter=mockSpanExporter, "
+ "exportUnsampledSpans=false, "
+ "scheduleDelayNanos=5000000000, " + "scheduleDelayNanos=5000000000, "
+ "maxExportBatchSize=512, " + "maxExportBatchSize=512, "
+ "exporterTimeoutNanos=30000000000}"); + "exporterTimeoutNanos=30000000000}");

View File

@ -59,7 +59,12 @@ class SimpleSpanProcessorTest {
SpanId.getInvalid(), SpanId.getInvalid(),
TraceFlags.getSampled(), TraceFlags.getSampled(),
TraceState.getDefault()); TraceState.getDefault());
private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT = SpanContext.getInvalid(); private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT =
SpanContext.create(
TraceId.getInvalid(),
SpanId.getInvalid(),
TraceFlags.getDefault(),
TraceState.getDefault());
private SpanProcessor simpleSampledSpansProcessor; private SpanProcessor simpleSampledSpansProcessor;
@ -100,29 +105,29 @@ class SimpleSpanProcessorTest {
} }
@Test @Test
void onEndSync_OnlySampled_NotSampledSpan() { void onEndSync_ExportUnsampledSpans_NotSampledSpan() {
SpanData spanData = TestUtils.makeBasicSpan();
when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT);
when(readableSpan.toSpanData()) when(readableSpan.toSpanData()).thenReturn(spanData);
.thenReturn(TestUtils.makeBasicSpan()) SpanProcessor simpleSpanProcessor =
.thenThrow(new RuntimeException()); SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
simpleSpanProcessor.onEnd(readableSpan); simpleSpanProcessor.onEnd(readableSpan);
verifyNoInteractions(spanExporter); verify(spanExporter).export(Collections.singletonList(spanData));
} }
@Test @Test
void onEndSync_OnlySampled_SampledSpan() { void onEndSync_ExportUnsampledSpans_SampledSpan() {
SpanData spanData = TestUtils.makeBasicSpan();
when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT);
when(readableSpan.toSpanData()) when(readableSpan.toSpanData()).thenReturn(spanData);
.thenReturn(TestUtils.makeBasicSpan()) SpanProcessor simpleSpanProcessor =
.thenThrow(new RuntimeException()); SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
simpleSpanProcessor.onEnd(readableSpan); simpleSpanProcessor.onEnd(readableSpan);
verify(spanExporter).export(Collections.singletonList(TestUtils.makeBasicSpan())); verify(spanExporter).export(Collections.singletonList(spanData));
} }
@Test @Test
void tracerSdk_NotSampled_Span() { void tracerSdk_SampledSpan() {
WaitingSpanExporter waitingSpanExporter = WaitingSpanExporter waitingSpanExporter =
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
@ -159,25 +164,43 @@ class SimpleSpanProcessorTest {
} }
@Test @Test
void tracerSdk_NotSampled_RecordingEventsSpan() { void tracerSdk_ExportUnsampledSpans_NotSampledSpan() {
// TODO(bdrutu): Fix this when Sampler return RECORD_ONLY option. WaitingSpanExporter waitingSpanExporter =
/* new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
tracer.addSpanProcessor(
BatchSpanProcessor.builder(waitingSpanExporter)
.setScheduleDelayMillis(MAX_SCHEDULE_DELAY_MILLIS)
.reportOnlySampled(false)
.build());
io.opentelemetry.trace.Span span = SdkTracerProvider sdkTracerProvider =
tracer SdkTracerProvider.builder()
.spanBuilder("FOO") .addSpanProcessor(
.setSampler(Samplers.neverSample()) SimpleSpanProcessor.builder(waitingSpanExporter)
.startSpanWithSampler(); .setExportUnsampledSpans(true)
span.end(); .build())
.setSampler(mockSampler)
.build();
List<SpanData> exported = waitingSpanExporter.waitForExport(1); when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData()); .thenReturn(SamplingResult.drop());
*/
try {
Tracer tracer = sdkTracerProvider.get(getClass().getName());
tracer.spanBuilder(SPAN_NAME).startSpan();
tracer.spanBuilder(SPAN_NAME).startSpan();
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
.thenReturn(SamplingResult.recordOnly());
Span span = tracer.spanBuilder(SPAN_NAME).startSpan();
span.end();
// Spans are recorded and exported in the same order as they are ended, we test that a non
// sampled span is not exported by creating and ending a sampled span after a non sampled span
// and checking that the first exported span is the sampled span (the non sampled did not get
// exported).
List<SpanData> exported = waitingSpanExporter.waitForExport();
// Need to check this because otherwise the variable span1 is unused, other option is to not
// have a span1 variable.
assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData());
} finally {
sdkTracerProvider.shutdown();
}
} }
@Test @Test