Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to export unsampled spans from span processors #6057

Merged
merged 10 commits into from
Jan 4, 2024
12 changes: 11 additions & 1 deletion docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
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)
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ void stringRepresentation() {
+ "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
+ "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}, "
+ "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}}"
+ "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}, exportUnsampledSpans=false}"
+ "}, "
+ "meterProvider=SdkMeterProvider{"
+ "clock=SystemClock{}, "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class BatchSpanProcessor implements SpanProcessor {
AttributeKey.booleanKey("dropped");
private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName();

private final boolean exportUnsampledSpans;
private final Worker worker;
private final AtomicBoolean isShutdown = new AtomicBoolean(false);

Expand All @@ -69,11 +70,13 @@ public static BatchSpanProcessorBuilder builder(SpanExporter spanExporter) {

BatchSpanProcessor(
SpanExporter spanExporter,
boolean exportUnsampledSpans,
MeterProvider meterProvider,
long scheduleDelayNanos,
int maxQueueSize,
int maxExportBatchSize,
long exporterTimeoutNanos) {
this.exportUnsampledSpans = exportUnsampledSpans;
this.worker =
new Worker(
spanExporter,
Expand All @@ -96,10 +99,9 @@ public boolean isStartRequired() {

@Override
public void onEnd(ReadableSpan span) {
if (span == null || !span.getSpanContext().isSampled()) {
return;
if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
worker.addSpan(span);
}
worker.addSpan(span);
}

@Override
Expand Down Expand Up @@ -135,6 +137,8 @@ public String toString() {
return "BatchSpanProcessor{"
+ "spanExporter="
+ worker.spanExporter
+ ", exportUnsampledSpans="
+ exportUnsampledSpans
+ ", scheduleDelayNanos="
+ worker.scheduleDelayNanos
+ ", maxExportBatchSize="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class BatchSpanProcessorBuilder {
static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000;

private final SpanExporter spanExporter;
private boolean exportUnsampledSpans;
private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS);
private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE;
Expand All @@ -35,6 +36,15 @@ public final class BatchSpanProcessorBuilder {
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
* DEFAULT_SCHEDULE_DELAY_MILLIS}ms.
Expand Down Expand Up @@ -146,6 +156,7 @@ int getMaxExportBatchSize() {
public BatchSpanProcessor build() {
return new BatchSpanProcessor(
spanExporter,
exportUnsampledSpans,
meterProvider,
scheduleDelayNanos,
maxQueueSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
private static final Logger logger = Logger.getLogger(SimpleSpanProcessor.class.getName());

private final SpanExporter spanExporter;
private final boolean sampled;
private final boolean exportUnsampledSpans;
private final Set<CompletableResultCode> pendingExports =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
Expand All @@ -53,12 +53,17 @@
*/
public static SpanProcessor create(SpanExporter 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.sampled = sampled;
this.exportUnsampledSpans = exportUnsampledSpans;
}

@Override
Expand All @@ -73,22 +78,21 @@

@Override
public void onEnd(ReadableSpan span) {
if (sampled && !span.getSpanContext().isSampled()) {
return;
}
try {
List<SpanData> spans = Collections.singletonList(span.toSpanData());
CompletableResultCode result = spanExporter.export(spans);
pendingExports.add(result);
result.whenComplete(
() -> {
pendingExports.remove(result);
if (!result.isSuccess()) {
logger.log(Level.FINE, "Exporter failed");
}
});
} catch (RuntimeException e) {
logger.log(Level.WARNING, "Exporter threw an Exception", e);
if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
try {
List<SpanData> spans = Collections.singletonList(span.toSpanData());
CompletableResultCode result = spanExporter.export(spans);
pendingExports.add(result);
result.whenComplete(
() -> {
pendingExports.remove(result);
if (!result.isSuccess()) {
logger.log(Level.FINE, "Exporter failed");
}
});
} catch (RuntimeException e) {
logger.log(Level.WARNING, "Exporter threw an Exception", e);

Check warning on line 94 in sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java

View check run for this annotation

Codecov / codecov/patch

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java#L93-L94

Added lines #L93 - L94 were not covered by tests
}
}
}

Expand Down Expand Up @@ -128,6 +132,11 @@

@Override
public String toString() {
return "SimpleSpanProcessor{" + "spanExporter=" + spanExporter + '}';
return "SimpleSpanProcessor{"
+ "spanExporter="
+ spanExporter
+ ", exportUnsampledSpans="
+ exportUnsampledSpans
+ '}';
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,38 @@ void exportNotSampledSpans_recordOnly() {
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
@Timeout(10)
@SuppressLogger(SdkTracerProvider.class)
Expand Down Expand Up @@ -569,6 +601,7 @@ void stringRepresentation() {
.hasToString(
"BatchSpanProcessor{"
+ "spanExporter=mockSpanExporter, "
+ "exportUnsampledSpans=false, "
+ "scheduleDelayNanos=5000000000, "
+ "maxExportBatchSize=512, "
+ "exporterTimeoutNanos=30000000000}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ class SimpleSpanProcessorTest {
SpanId.getInvalid(),
TraceFlags.getSampled(),
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;

Expand Down Expand Up @@ -100,29 +105,29 @@ void onEndSync_NotSampledSpan() {
}

@Test
void onEndSync_OnlySampled_NotSampledSpan() {
void onEndSync_ExportUnsampledSpans_NotSampledSpan() {
SpanData spanData = TestUtils.makeBasicSpan();
when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT);
when(readableSpan.toSpanData())
.thenReturn(TestUtils.makeBasicSpan())
.thenThrow(new RuntimeException());
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
when(readableSpan.toSpanData()).thenReturn(spanData);
SpanProcessor simpleSpanProcessor =
SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
simpleSpanProcessor.onEnd(readableSpan);
verifyNoInteractions(spanExporter);
verify(spanExporter).export(Collections.singletonList(spanData));
}

@Test
void onEndSync_OnlySampled_SampledSpan() {
void onEndSync_ExportUnsampledSpans_SampledSpan() {
SpanData spanData = TestUtils.makeBasicSpan();
when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT);
when(readableSpan.toSpanData())
.thenReturn(TestUtils.makeBasicSpan())
.thenThrow(new RuntimeException());
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
when(readableSpan.toSpanData()).thenReturn(spanData);
SpanProcessor simpleSpanProcessor =
SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
simpleSpanProcessor.onEnd(readableSpan);
verify(spanExporter).export(Collections.singletonList(TestUtils.makeBasicSpan()));
verify(spanExporter).export(Collections.singletonList(spanData));
}

@Test
void tracerSdk_NotSampled_Span() {
void tracerSdk_SampledSpan() {
WaitingSpanExporter waitingSpanExporter =
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());

Expand Down Expand Up @@ -159,25 +164,43 @@ void tracerSdk_NotSampled_Span() {
}

@Test
void tracerSdk_NotSampled_RecordingEventsSpan() {
// TODO(bdrutu): Fix this when Sampler return RECORD_ONLY option.
/*
tracer.addSpanProcessor(
BatchSpanProcessor.builder(waitingSpanExporter)
.setScheduleDelayMillis(MAX_SCHEDULE_DELAY_MILLIS)
.reportOnlySampled(false)
.build());

io.opentelemetry.trace.Span span =
tracer
.spanBuilder("FOO")
.setSampler(Samplers.neverSample())
.startSpanWithSampler();
span.end();

List<SpanData> exported = waitingSpanExporter.waitForExport(1);
assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData());
*/
void tracerSdk_ExportUnsampledSpans_NotSampledSpan() {
WaitingSpanExporter waitingSpanExporter =
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());

SdkTracerProvider sdkTracerProvider =
SdkTracerProvider.builder()
.addSpanProcessor(
SimpleSpanProcessor.builder(waitingSpanExporter)
.setExportUnsampledSpans(true)
.build())
.setSampler(mockSampler)
.build();

when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
.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
Expand Down
Loading