Skip to content

Commit 929283f

Browse files
committed
Support overriding OTel SpanExporters
See gh-35596
1 parent d515599 commit 929283f

File tree

4 files changed

+199
-13
lines changed

4 files changed

+199
-13
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,22 @@ SpanProcessors spanProcessors(ObjectProvider<SpanProcessor> spanProcessors) {
133133
}
134134

135135
@Bean
136-
BatchSpanProcessor otelSpanProcessor(ObjectProvider<SpanExporter> spanExporters,
136+
BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters,
137137
ObjectProvider<SpanExportingPredicate> spanExportingPredicates, ObjectProvider<SpanReporter> spanReporters,
138138
ObjectProvider<SpanFilter> spanFilters, ObjectProvider<MeterProvider> meterProvider) {
139-
BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(new CompositeSpanExporter(
140-
spanExporters.orderedStream().toList(), spanExportingPredicates.orderedStream().toList(),
141-
spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList()));
139+
BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(
140+
new CompositeSpanExporter(spanExporters.getList(), spanExportingPredicates.orderedStream().toList(),
141+
spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList()));
142142
meterProvider.ifAvailable(builder::setMeterProvider);
143143
return builder.build();
144144
}
145145

146+
@Bean
147+
@ConditionalOnMissingBean
148+
SpanExporters spanExporters(ObjectProvider<SpanExporter> spanExporters) {
149+
return SpanExporters.of(spanExporters.orderedStream().collect(Collectors.toList()));
150+
}
151+
146152
@Bean
147153
@ConditionalOnMissingBean
148154
Tracer otelTracer(OpenTelemetry openTelemetry) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.tracing;
18+
19+
import java.util.Arrays;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.Spliterator;
23+
24+
import io.opentelemetry.sdk.trace.export.SpanExporter;
25+
26+
/**
27+
* A collection of {@link SpanExporter span exporters}.
28+
*
29+
* @author Moritz Halbritter
30+
* @since 3.2.0
31+
*/
32+
public interface SpanExporters extends Iterable<SpanExporter> {
33+
34+
/**
35+
* Returns the list of {@link SpanExporter span exporters}.
36+
* @return the list of span exporters
37+
*/
38+
List<SpanExporter> getList();
39+
40+
@Override
41+
default Iterator<SpanExporter> iterator() {
42+
return getList().iterator();
43+
}
44+
45+
@Override
46+
default Spliterator<SpanExporter> spliterator() {
47+
return getList().spliterator();
48+
}
49+
50+
/**
51+
* Constructs a {@link SpanExporters} instance with the given list of
52+
* {@link SpanExporter span exporters}.
53+
* @param spanExporters the list of span exporters
54+
* @return the constructed {@link SpanExporters} instance
55+
*/
56+
static SpanExporters of(List<SpanExporter> spanExporters) {
57+
return () -> spanExporters;
58+
}
59+
60+
/**
61+
* Constructs a {@link SpanExporters} instance with the given {@link SpanExporter span
62+
* exporters}.
63+
* @param spanExporters the span exporters
64+
* @return the constructed {@link SpanExporters} instance
65+
*/
66+
static SpanExporters of(SpanExporter... spanExporters) {
67+
return of(Arrays.asList(spanExporters));
68+
}
69+
70+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.actuate.autoconfigure.tracing;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collection;
2021
import java.util.List;
2122

2223
import io.micrometer.tracing.SpanCustomizer;
@@ -35,9 +36,12 @@
3536
import io.opentelemetry.context.propagation.ContextPropagators;
3637
import io.opentelemetry.context.propagation.TextMapPropagator;
3738
import io.opentelemetry.extension.trace.propagation.B3Propagator;
39+
import io.opentelemetry.sdk.common.CompletableResultCode;
3840
import io.opentelemetry.sdk.trace.SdkTracerProvider;
3941
import io.opentelemetry.sdk.trace.SpanLimits;
4042
import io.opentelemetry.sdk.trace.SpanProcessor;
43+
import io.opentelemetry.sdk.trace.data.SpanData;
44+
import io.opentelemetry.sdk.trace.export.SpanExporter;
4145
import io.opentelemetry.sdk.trace.samplers.Sampler;
4246
import org.junit.jupiter.api.Test;
4347
import org.junit.jupiter.params.ParameterizedTest;
@@ -88,6 +92,7 @@ void shouldSupplyBeans() {
8892
assertThat(context).hasSingleBean(TextMapPropagator.class);
8993
assertThat(context).hasSingleBean(OtelSpanCustomizer.class);
9094
assertThat(context).hasSingleBean(SpanProcessors.class);
95+
assertThat(context).hasSingleBean(SpanExporters.class);
9196
});
9297
}
9398

@@ -119,6 +124,7 @@ void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
119124
assertThat(context).doesNotHaveBean(TextMapPropagator.class);
120125
assertThat(context).doesNotHaveBean(OtelSpanCustomizer.class);
121126
assertThat(context).doesNotHaveBean(SpanProcessors.class);
127+
assertThat(context).doesNotHaveBean(SpanExporters.class);
122128
});
123129
}
124130

@@ -151,6 +157,8 @@ void shouldBackOffOnCustomBeans() {
151157
assertThat(context).hasSingleBean(SpanCustomizer.class);
152158
assertThat(context).hasBean("customSpanProcessors");
153159
assertThat(context).hasSingleBean(SpanProcessors.class);
160+
assertThat(context).hasBean("customSpanExporters");
161+
assertThat(context).hasSingleBean(SpanExporters.class);
154162
});
155163
}
156164

@@ -164,6 +172,17 @@ void shouldAllowMultipleSpanProcessors() {
164172
});
165173
}
166174

175+
@Test
176+
void shouldAllowMultipleSpanExporters() {
177+
this.contextRunner.withUserConfiguration(MultipleSpanExporterConfiguration.class).run((context) -> {
178+
assertThat(context.getBeansOfType(SpanExporter.class)).hasSize(2);
179+
assertThat(context).hasBean("spanExporter1");
180+
assertThat(context).hasBean("spanExporter2");
181+
SpanExporters spanExporters = context.getBean(SpanExporters.class);
182+
assertThat(spanExporters).hasSize(2);
183+
});
184+
}
185+
167186
@Test
168187
void shouldAllowMultipleTextMapPropagators() {
169188
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
@@ -228,15 +247,6 @@ void shouldSupplyW3CPropagationWithoutBaggageWhenDisabled() {
228247
});
229248
}
230249

231-
private List<TextMapPropagator> getInjectors(TextMapPropagator propagator) {
232-
assertThat(propagator).as("propagator").isNotNull();
233-
if (propagator instanceof CompositeTextMapPropagator compositePropagator) {
234-
return compositePropagator.getInjectors().stream().toList();
235-
}
236-
fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass()));
237-
throw new AssertionError("Unreachable");
238-
}
239-
240250
@Test
241251
void shouldCustomizeSdkTracerProvider() {
242252
this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> {
@@ -255,6 +265,15 @@ void defaultSpanProcessorShouldUseMeterProviderIfAvailable() {
255265
});
256266
}
257267

268+
private List<TextMapPropagator> getInjectors(TextMapPropagator propagator) {
269+
assertThat(propagator).as("propagator").isNotNull();
270+
if (propagator instanceof CompositeTextMapPropagator compositePropagator) {
271+
return compositePropagator.getInjectors().stream().toList();
272+
}
273+
fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass()));
274+
throw new AssertionError("Unreachable");
275+
}
276+
258277
@Configuration(proxyBeanMethods = false)
259278
private static class MeterProviderConfiguration {
260279

@@ -278,6 +297,21 @@ SpanProcessor customSpanProcessor() {
278297

279298
}
280299

300+
@Configuration(proxyBeanMethods = false)
301+
private static class MultipleSpanExporterConfiguration {
302+
303+
@Bean
304+
SpanExporter spanExporter1() {
305+
return new DummySpanExporter();
306+
}
307+
308+
@Bean
309+
SpanExporter spanExporter2() {
310+
return new DummySpanExporter();
311+
}
312+
313+
}
314+
281315
@Configuration(proxyBeanMethods = false)
282316
private static class CustomConfiguration {
283317

@@ -286,6 +320,11 @@ SpanProcessors customSpanProcessors() {
286320
return SpanProcessors.of(mock(SpanProcessor.class));
287321
}
288322

323+
@Bean
324+
SpanExporters customSpanExporters() {
325+
return SpanExporters.of(new DummySpanExporter());
326+
}
327+
289328
@Bean
290329
io.micrometer.tracing.Tracer customMicrometerTracer() {
291330
return mock(io.micrometer.tracing.Tracer.class);
@@ -381,4 +420,23 @@ SdkTracerProviderBuilderCustomizer sdkTracerProviderBuilderCustomizerTwo() {
381420

382421
}
383422

423+
private static class DummySpanExporter implements SpanExporter {
424+
425+
@Override
426+
public CompletableResultCode export(Collection<SpanData> spans) {
427+
return CompletableResultCode.ofSuccess();
428+
}
429+
430+
@Override
431+
public CompletableResultCode flush() {
432+
return CompletableResultCode.ofSuccess();
433+
}
434+
435+
@Override
436+
public CompletableResultCode shutdown() {
437+
return CompletableResultCode.ofSuccess();
438+
}
439+
440+
}
441+
384442
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.tracing;
18+
19+
import java.util.List;
20+
21+
import io.opentelemetry.sdk.trace.export.SpanExporter;
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.mockito.Mockito.mock;
26+
27+
/**
28+
* Tests for {@link SpanExporters}.
29+
*
30+
* @author Moritz Halbritter
31+
*/
32+
class SpanExportersTests {
33+
34+
@Test
35+
void ofList() {
36+
SpanExporter spanExporter1 = mock(SpanExporter.class);
37+
SpanExporter spanExporter2 = mock(SpanExporter.class);
38+
SpanExporters spanExporters = SpanExporters.of(List.of(spanExporter1, spanExporter2));
39+
assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2);
40+
assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2);
41+
}
42+
43+
@Test
44+
void ofArray() {
45+
SpanExporter spanExporter1 = mock(SpanExporter.class);
46+
SpanExporter spanExporter2 = mock(SpanExporter.class);
47+
SpanExporters spanExporters = SpanExporters.of(spanExporter1, spanExporter2);
48+
assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2);
49+
assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2);
50+
}
51+
52+
}

0 commit comments

Comments
 (0)