diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index b86f1eeda4..4220d31c0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -133,16 +133,22 @@ public class OpenTelemetryAutoConfiguration { } @Bean - BatchSpanProcessor otelSpanProcessor(ObjectProvider spanExporters, + BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, ObjectProvider spanExportingPredicates, ObjectProvider spanReporters, ObjectProvider spanFilters, ObjectProvider meterProvider) { - BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(new CompositeSpanExporter( - spanExporters.orderedStream().toList(), spanExportingPredicates.orderedStream().toList(), - spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); + BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder( + new CompositeSpanExporter(spanExporters.getList(), spanExportingPredicates.orderedStream().toList(), + spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } + @Bean + @ConditionalOnMissingBean + SpanExporters spanExporters(ObjectProvider spanExporters) { + return SpanExporters.of(spanExporters.orderedStream().collect(Collectors.toList())); + } + @Bean @ConditionalOnMissingBean Tracer otelTracer(OpenTelemetry openTelemetry) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java new file mode 100644 index 0000000000..b6f133e57b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.tracing; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; + +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * A collection of {@link SpanExporter span exporters}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public interface SpanExporters extends Iterable { + + /** + * Returns the list of {@link SpanExporter span exporters}. + * @return the list of span exporters + */ + List getList(); + + @Override + default Iterator iterator() { + return getList().iterator(); + } + + @Override + default Spliterator spliterator() { + return getList().spliterator(); + } + + /** + * Constructs a {@link SpanExporters} instance with the given list of + * {@link SpanExporter span exporters}. + * @param spanExporters the list of span exporters + * @return the constructed {@link SpanExporters} instance + */ + static SpanExporters of(List spanExporters) { + return () -> spanExporters; + } + + /** + * Constructs a {@link SpanExporters} instance with the given {@link SpanExporter span + * exporters}. + * @param spanExporters the span exporters + * @return the constructed {@link SpanExporters} instance + */ + static SpanExporters of(SpanExporter... spanExporters) { + return of(Arrays.asList(spanExporters)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 2bb1b24924..e757017653 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import io.micrometer.tracing.SpanCustomizer; @@ -35,9 +36,12 @@ import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SpanLimits; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -88,6 +92,7 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(TextMapPropagator.class); assertThat(context).hasSingleBean(OtelSpanCustomizer.class); assertThat(context).hasSingleBean(SpanProcessors.class); + assertThat(context).hasSingleBean(SpanExporters.class); }); } @@ -119,6 +124,7 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).doesNotHaveBean(TextMapPropagator.class); assertThat(context).doesNotHaveBean(OtelSpanCustomizer.class); assertThat(context).doesNotHaveBean(SpanProcessors.class); + assertThat(context).doesNotHaveBean(SpanExporters.class); }); } @@ -151,6 +157,8 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(SpanCustomizer.class); assertThat(context).hasBean("customSpanProcessors"); assertThat(context).hasSingleBean(SpanProcessors.class); + assertThat(context).hasBean("customSpanExporters"); + assertThat(context).hasSingleBean(SpanExporters.class); }); } @@ -164,6 +172,17 @@ class OpenTelemetryAutoConfigurationTests { }); } + @Test + void shouldAllowMultipleSpanExporters() { + this.contextRunner.withUserConfiguration(MultipleSpanExporterConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(SpanExporter.class)).hasSize(2); + assertThat(context).hasBean("spanExporter1"); + assertThat(context).hasBean("spanExporter2"); + SpanExporters spanExporters = context.getBean(SpanExporters.class); + assertThat(spanExporters).hasSize(2); + }); + } + @Test void shouldAllowMultipleTextMapPropagators() { this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { @@ -228,15 +247,6 @@ class OpenTelemetryAutoConfigurationTests { }); } - private List getInjectors(TextMapPropagator propagator) { - assertThat(propagator).as("propagator").isNotNull(); - if (propagator instanceof CompositeTextMapPropagator compositePropagator) { - return compositePropagator.getInjectors().stream().toList(); - } - fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass())); - throw new AssertionError("Unreachable"); - } - @Test void shouldCustomizeSdkTracerProvider() { this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> { @@ -255,6 +265,15 @@ class OpenTelemetryAutoConfigurationTests { }); } + private List getInjectors(TextMapPropagator propagator) { + assertThat(propagator).as("propagator").isNotNull(); + if (propagator instanceof CompositeTextMapPropagator compositePropagator) { + return compositePropagator.getInjectors().stream().toList(); + } + fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass())); + throw new AssertionError("Unreachable"); + } + @Configuration(proxyBeanMethods = false) private static class MeterProviderConfiguration { @@ -278,6 +297,21 @@ class OpenTelemetryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + private static class MultipleSpanExporterConfiguration { + + @Bean + SpanExporter spanExporter1() { + return new DummySpanExporter(); + } + + @Bean + SpanExporter spanExporter2() { + return new DummySpanExporter(); + } + + } + @Configuration(proxyBeanMethods = false) private static class CustomConfiguration { @@ -286,6 +320,11 @@ class OpenTelemetryAutoConfigurationTests { return SpanProcessors.of(mock(SpanProcessor.class)); } + @Bean + SpanExporters customSpanExporters() { + return SpanExporters.of(new DummySpanExporter()); + } + @Bean io.micrometer.tracing.Tracer customMicrometerTracer() { return mock(io.micrometer.tracing.Tracer.class); @@ -381,4 +420,23 @@ class OpenTelemetryAutoConfigurationTests { } + private static class DummySpanExporter implements SpanExporter { + + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java new file mode 100644 index 0000000000..dc883b9718 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.tracing; + +import java.util.List; + +import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SpanExporters}. + * + * @author Moritz Halbritter + */ +class SpanExportersTests { + + @Test + void ofList() { + SpanExporter spanExporter1 = mock(SpanExporter.class); + SpanExporter spanExporter2 = mock(SpanExporter.class); + SpanExporters spanExporters = SpanExporters.of(List.of(spanExporter1, spanExporter2)); + assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + } + + @Test + void ofArray() { + SpanExporter spanExporter1 = mock(SpanExporter.class); + SpanExporter spanExporter2 = mock(SpanExporter.class); + SpanExporters spanExporters = SpanExporters.of(spanExporter1, spanExporter2); + assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + } + +}