Skip to content

Commit 07351a2

Browse files
HaloFourjack-berg
andauthored
Add option to export unsampled spans from span processors (#6057)
Co-authored-by: jack-berg <[email protected]>
1 parent f4b5bbe commit 07351a2

File tree

8 files changed

+185
-59
lines changed

8 files changed

+185
-59
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
Comparing source compatibility of against
2-
No changes.
2+
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder (not serializable)
3+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
4+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder setExportUnsampledSpans(boolean)
5+
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.SimpleSpanProcessor (not serializable)
6+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
7+
+++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder builder(io.opentelemetry.sdk.trace.export.SpanExporter)
8+
+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder (not serializable)
9+
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
10+
+++ NEW SUPERCLASS: java.lang.Object
11+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessor build()
12+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder setExportUnsampledSpans(boolean)

sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ void stringRepresentation() {
415415
+ "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
416416
+ "spanLimitsSupplier=SpanLimitsValue{maxNumberOfAttributes=128, maxNumberOfEvents=128, maxNumberOfLinks=128, maxNumberOfAttributesPerEvent=128, maxNumberOfAttributesPerLink=128, maxAttributeValueLength=2147483647}, "
417417
+ "sampler=ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}, "
418-
+ "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}}"
418+
+ "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}, exportUnsampledSpans=false}"
419419
+ "}, "
420420
+ "meterProvider=SdkMeterProvider{"
421421
+ "clock=SystemClock{}, "

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessor.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public final class BatchSpanProcessor implements SpanProcessor {
5353
AttributeKey.booleanKey("dropped");
5454
private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName();
5555

56+
private final boolean exportUnsampledSpans;
5657
private final Worker worker;
5758
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
5859

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

7071
BatchSpanProcessor(
7172
SpanExporter spanExporter,
73+
boolean exportUnsampledSpans,
7274
MeterProvider meterProvider,
7375
long scheduleDelayNanos,
7476
int maxQueueSize,
7577
int maxExportBatchSize,
7678
long exporterTimeoutNanos) {
79+
this.exportUnsampledSpans = exportUnsampledSpans;
7780
this.worker =
7881
new Worker(
7982
spanExporter,
@@ -96,10 +99,9 @@ public boolean isStartRequired() {
9699

97100
@Override
98101
public void onEnd(ReadableSpan span) {
99-
if (span == null || !span.getSpanContext().isSampled()) {
100-
return;
102+
if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
103+
worker.addSpan(span);
101104
}
102-
worker.addSpan(span);
103105
}
104106

105107
@Override
@@ -135,6 +137,8 @@ public String toString() {
135137
return "BatchSpanProcessor{"
136138
+ "spanExporter="
137139
+ worker.spanExporter
140+
+ ", exportUnsampledSpans="
141+
+ exportUnsampledSpans
138142
+ ", scheduleDelayNanos="
139143
+ worker.scheduleDelayNanos
140144
+ ", maxExportBatchSize="

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBuilder.java

+11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public final class BatchSpanProcessorBuilder {
2525
static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000;
2626

2727
private final SpanExporter spanExporter;
28+
private boolean exportUnsampledSpans;
2829
private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS);
2930
private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
3031
private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE;
@@ -35,6 +36,15 @@ public final class BatchSpanProcessorBuilder {
3536
this.spanExporter = requireNonNull(spanExporter, "spanExporter");
3637
}
3738

39+
/**
40+
* Sets whether unsampled spans should be exported. If unset, defaults to exporting only sampled
41+
* spans.
42+
*/
43+
public BatchSpanProcessorBuilder setExportUnsampledSpans(boolean exportUnsampledSpans) {
44+
this.exportUnsampledSpans = exportUnsampledSpans;
45+
return this;
46+
}
47+
3848
/**
3949
* Sets the delay interval between two consecutive exports. If unset, defaults to {@value
4050
* DEFAULT_SCHEDULE_DELAY_MILLIS}ms.
@@ -146,6 +156,7 @@ int getMaxExportBatchSize() {
146156
public BatchSpanProcessor build() {
147157
return new BatchSpanProcessor(
148158
spanExporter,
159+
exportUnsampledSpans,
149160
meterProvider,
150161
scheduleDelayNanos,
151162
maxQueueSize,

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java

+30-21
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public final class SimpleSpanProcessor implements SpanProcessor {
3636
private static final Logger logger = Logger.getLogger(SimpleSpanProcessor.class.getName());
3737

3838
private final SpanExporter spanExporter;
39-
private final boolean sampled;
39+
private final boolean exportUnsampledSpans;
4040
private final Set<CompletableResultCode> pendingExports =
4141
Collections.newSetFromMap(new ConcurrentHashMap<>());
4242
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
@@ -53,12 +53,17 @@ public final class SimpleSpanProcessor implements SpanProcessor {
5353
*/
5454
public static SpanProcessor create(SpanExporter exporter) {
5555
requireNonNull(exporter, "exporter");
56-
return new SimpleSpanProcessor(exporter, /* sampled= */ true);
56+
return builder(exporter).build();
5757
}
5858

59-
SimpleSpanProcessor(SpanExporter spanExporter, boolean sampled) {
59+
public static SimpleSpanProcessorBuilder builder(SpanExporter exporter) {
60+
requireNonNull(exporter, "exporter");
61+
return new SimpleSpanProcessorBuilder(exporter);
62+
}
63+
64+
SimpleSpanProcessor(SpanExporter spanExporter, boolean exportUnsampledSpans) {
6065
this.spanExporter = requireNonNull(spanExporter, "spanExporter");
61-
this.sampled = sampled;
66+
this.exportUnsampledSpans = exportUnsampledSpans;
6267
}
6368

6469
@Override
@@ -73,22 +78,21 @@ public boolean isStartRequired() {
7378

7479
@Override
7580
public void onEnd(ReadableSpan span) {
76-
if (sampled && !span.getSpanContext().isSampled()) {
77-
return;
78-
}
79-
try {
80-
List<SpanData> spans = Collections.singletonList(span.toSpanData());
81-
CompletableResultCode result = spanExporter.export(spans);
82-
pendingExports.add(result);
83-
result.whenComplete(
84-
() -> {
85-
pendingExports.remove(result);
86-
if (!result.isSuccess()) {
87-
logger.log(Level.FINE, "Exporter failed");
88-
}
89-
});
90-
} catch (RuntimeException e) {
91-
logger.log(Level.WARNING, "Exporter threw an Exception", e);
81+
if (span != null && (exportUnsampledSpans || span.getSpanContext().isSampled())) {
82+
try {
83+
List<SpanData> spans = Collections.singletonList(span.toSpanData());
84+
CompletableResultCode result = spanExporter.export(spans);
85+
pendingExports.add(result);
86+
result.whenComplete(
87+
() -> {
88+
pendingExports.remove(result);
89+
if (!result.isSuccess()) {
90+
logger.log(Level.FINE, "Exporter failed");
91+
}
92+
});
93+
} catch (RuntimeException e) {
94+
logger.log(Level.WARNING, "Exporter threw an Exception", e);
95+
}
9296
}
9397
}
9498

@@ -128,6 +132,11 @@ public CompletableResultCode forceFlush() {
128132

129133
@Override
130134
public String toString() {
131-
return "SimpleSpanProcessor{" + "spanExporter=" + spanExporter + '}';
135+
return "SimpleSpanProcessor{"
136+
+ "spanExporter="
137+
+ spanExporter
138+
+ ", exportUnsampledSpans="
139+
+ exportUnsampledSpans
140+
+ '}';
132141
}
133142
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.trace.export;
7+
8+
import static java.util.Objects.requireNonNull;
9+
10+
/** Builder class for {@link SimpleSpanProcessor}. */
11+
public final class SimpleSpanProcessorBuilder {
12+
private final SpanExporter spanExporter;
13+
private boolean exportUnsampledSpans;
14+
15+
SimpleSpanProcessorBuilder(SpanExporter spanExporter) {
16+
this.spanExporter = requireNonNull(spanExporter, "spanExporter");
17+
}
18+
19+
/**
20+
* Sets whether unsampled spans should be exported. If unset, defaults to exporting only sampled
21+
* spans.
22+
*/
23+
public SimpleSpanProcessorBuilder setExportUnsampledSpans(boolean exportUnsampledSpans) {
24+
this.exportUnsampledSpans = exportUnsampledSpans;
25+
return this;
26+
}
27+
28+
/**
29+
* Returns a new {@link SimpleSpanProcessor} with the configuration of this builder.
30+
*
31+
* @return a new {@link SimpleSpanProcessor}.
32+
*/
33+
public SimpleSpanProcessor build() {
34+
return new SimpleSpanProcessor(spanExporter, exportUnsampledSpans);
35+
}
36+
}

sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,38 @@ void exportNotSampledSpans_recordOnly() {
517517
assertThat(exported).containsExactly(span.toSpanData());
518518
}
519519

520+
@Test
521+
void exportUnsampledSpans_recordOnly() {
522+
WaitingSpanExporter waitingSpanExporter =
523+
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
524+
525+
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
526+
.thenReturn(SamplingResult.recordOnly());
527+
sdkTracerProvider =
528+
SdkTracerProvider.builder()
529+
.addSpanProcessor(
530+
BatchSpanProcessor.builder(waitingSpanExporter)
531+
.setExportUnsampledSpans(true)
532+
.setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS)
533+
.build())
534+
.setSampler(mockSampler)
535+
.build();
536+
537+
ReadableSpan span1 = createEndedSpan(SPAN_NAME_1);
538+
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
539+
.thenReturn(SamplingResult.recordAndSample());
540+
ReadableSpan span2 = createEndedSpan(SPAN_NAME_2);
541+
542+
// Spans are recorded and exported in the same order as they are ended, we test that a non
543+
// exported span is not exported by creating and ending a sampled span after a non sampled span
544+
// and checking that the first exported span is the sampled span (the non sampled did not get
545+
// exported).
546+
List<SpanData> exported = waitingSpanExporter.waitForExport();
547+
// Need to check this because otherwise the variable span1 is unused, other option is to not
548+
// have a span1 variable.
549+
assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData());
550+
}
551+
520552
@Test
521553
@Timeout(10)
522554
@SuppressLogger(SdkTracerProvider.class)
@@ -569,6 +601,7 @@ void stringRepresentation() {
569601
.hasToString(
570602
"BatchSpanProcessor{"
571603
+ "spanExporter=mockSpanExporter, "
604+
+ "exportUnsampledSpans=false, "
572605
+ "scheduleDelayNanos=5000000000, "
573606
+ "maxExportBatchSize=512, "
574607
+ "exporterTimeoutNanos=30000000000}");

sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java

+56-33
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ class SimpleSpanProcessorTest {
5959
SpanId.getInvalid(),
6060
TraceFlags.getSampled(),
6161
TraceState.getDefault());
62-
private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT = SpanContext.getInvalid();
62+
private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT =
63+
SpanContext.create(
64+
TraceId.getInvalid(),
65+
SpanId.getInvalid(),
66+
TraceFlags.getDefault(),
67+
TraceState.getDefault());
6368

6469
private SpanProcessor simpleSampledSpansProcessor;
6570

@@ -100,29 +105,29 @@ void onEndSync_NotSampledSpan() {
100105
}
101106

102107
@Test
103-
void onEndSync_OnlySampled_NotSampledSpan() {
108+
void onEndSync_ExportUnsampledSpans_NotSampledSpan() {
109+
SpanData spanData = TestUtils.makeBasicSpan();
104110
when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT);
105-
when(readableSpan.toSpanData())
106-
.thenReturn(TestUtils.makeBasicSpan())
107-
.thenThrow(new RuntimeException());
108-
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
111+
when(readableSpan.toSpanData()).thenReturn(spanData);
112+
SpanProcessor simpleSpanProcessor =
113+
SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
109114
simpleSpanProcessor.onEnd(readableSpan);
110-
verifyNoInteractions(spanExporter);
115+
verify(spanExporter).export(Collections.singletonList(spanData));
111116
}
112117

113118
@Test
114-
void onEndSync_OnlySampled_SampledSpan() {
119+
void onEndSync_ExportUnsampledSpans_SampledSpan() {
120+
SpanData spanData = TestUtils.makeBasicSpan();
115121
when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT);
116-
when(readableSpan.toSpanData())
117-
.thenReturn(TestUtils.makeBasicSpan())
118-
.thenThrow(new RuntimeException());
119-
SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter);
122+
when(readableSpan.toSpanData()).thenReturn(spanData);
123+
SpanProcessor simpleSpanProcessor =
124+
SimpleSpanProcessor.builder(spanExporter).setExportUnsampledSpans(true).build();
120125
simpleSpanProcessor.onEnd(readableSpan);
121-
verify(spanExporter).export(Collections.singletonList(TestUtils.makeBasicSpan()));
126+
verify(spanExporter).export(Collections.singletonList(spanData));
122127
}
123128

124129
@Test
125-
void tracerSdk_NotSampled_Span() {
130+
void tracerSdk_SampledSpan() {
126131
WaitingSpanExporter waitingSpanExporter =
127132
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
128133

@@ -159,25 +164,43 @@ void tracerSdk_NotSampled_Span() {
159164
}
160165

161166
@Test
162-
void tracerSdk_NotSampled_RecordingEventsSpan() {
163-
// TODO(bdrutu): Fix this when Sampler return RECORD_ONLY option.
164-
/*
165-
tracer.addSpanProcessor(
166-
BatchSpanProcessor.builder(waitingSpanExporter)
167-
.setScheduleDelayMillis(MAX_SCHEDULE_DELAY_MILLIS)
168-
.reportOnlySampled(false)
169-
.build());
170-
171-
io.opentelemetry.trace.Span span =
172-
tracer
173-
.spanBuilder("FOO")
174-
.setSampler(Samplers.neverSample())
175-
.startSpanWithSampler();
176-
span.end();
177-
178-
List<SpanData> exported = waitingSpanExporter.waitForExport(1);
179-
assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData());
180-
*/
167+
void tracerSdk_ExportUnsampledSpans_NotSampledSpan() {
168+
WaitingSpanExporter waitingSpanExporter =
169+
new WaitingSpanExporter(1, CompletableResultCode.ofSuccess());
170+
171+
SdkTracerProvider sdkTracerProvider =
172+
SdkTracerProvider.builder()
173+
.addSpanProcessor(
174+
SimpleSpanProcessor.builder(waitingSpanExporter)
175+
.setExportUnsampledSpans(true)
176+
.build())
177+
.setSampler(mockSampler)
178+
.build();
179+
180+
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
181+
.thenReturn(SamplingResult.drop());
182+
183+
try {
184+
Tracer tracer = sdkTracerProvider.get(getClass().getName());
185+
tracer.spanBuilder(SPAN_NAME).startSpan();
186+
tracer.spanBuilder(SPAN_NAME).startSpan();
187+
188+
when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList()))
189+
.thenReturn(SamplingResult.recordOnly());
190+
Span span = tracer.spanBuilder(SPAN_NAME).startSpan();
191+
span.end();
192+
193+
// Spans are recorded and exported in the same order as they are ended, we test that a non
194+
// sampled span is not exported by creating and ending a sampled span after a non sampled span
195+
// and checking that the first exported span is the sampled span (the non sampled did not get
196+
// exported).
197+
List<SpanData> exported = waitingSpanExporter.waitForExport();
198+
// Need to check this because otherwise the variable span1 is unused, other option is to not
199+
// have a span1 variable.
200+
assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData());
201+
} finally {
202+
sdkTracerProvider.shutdown();
203+
}
181204
}
182205

183206
@Test

0 commit comments

Comments
 (0)