diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 6ae4cf4528..0671717c0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -106,6 +106,7 @@ dependencies { optional("org.hibernate.orm:hibernate-micrometer") optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") + optional("org.junit.platform:junit-platform-launcher") optional("org.liquibase:liquibase-core") { exclude group: "javax.xml.bind", module: "jaxb-api" } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherApplicationListener.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansApplicationListener.java similarity index 64% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherApplicationListener.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansApplicationListener.java index eac080a07b..d43cf33428 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherApplicationListener.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansApplicationListener.java @@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.UnaryOperator; import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper; import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher; @@ -42,11 +41,17 @@ import org.springframework.util.MultiValueMap; /** * {@link ApplicationListener} to add an OpenTelemetry {@link ContextStorage} wrapper for * {@link EventPublisher} bean support. A single {@link ContextStorage} wrapper is added - * as early as possible then updated with {@link EventPublisher} beans as needed. + * on the {@link ApplicationStartingEvent} then updated with {@link EventPublisher} beans + * as needed. + *

+ * The {@link #addWrapper()} method may also be called directly if the + * {@link ApplicationStartingEvent} isn't called early enough or isn't fired. * * @author Phillip Webb + * @since 3.4.0 + * @see OpenTelemetryEventPublisherBeansTestExecutionListener */ -class OpenTelemetryEventPublisherApplicationListener implements GenericApplicationListener { +public class OpenTelemetryEventPublisherBeansApplicationListener implements GenericApplicationListener { private static final boolean OTEL_CONTEXT_PRESENT = ClassUtils.isPresent("io.opentelemetry.context.ContextStorage", null); @@ -54,6 +59,8 @@ class OpenTelemetryEventPublisherApplicationListener implements GenericApplicati private static final boolean MICROMETER_OTEL_PRESENT = ClassUtils .isPresent("io.micrometer.tracing.otel.bridge.OtelTracer", null); + private static final AtomicBoolean added = new AtomicBoolean(); + @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; @@ -69,11 +76,11 @@ class OpenTelemetryEventPublisherApplicationListener implements GenericApplicati @Override public void onApplicationEvent(ApplicationEvent event) { - if (!OTEL_CONTEXT_PRESENT || !MICROMETER_OTEL_PRESENT) { + if (!isInstallable()) { return; } if (event instanceof ApplicationStartingEvent) { - EventPublisherBeansContextWrapper.addWrapperIfNecessary(); + addWrapper(); } if (event instanceof ContextRefreshedEvent contextRefreshedEvent) { ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext(); @@ -83,92 +90,105 @@ class OpenTelemetryEventPublisherApplicationListener implements GenericApplicati .stream() .map(EventPublishingContextWrapper::new) .toList(); - EventPublisherBeansContextWrapper.instance.put(applicationContext, publishers); + Wrapper.instance.put(applicationContext, publishers); } if (event instanceof ContextClosedEvent contextClosedEvent) { - EventPublisherBeansContextWrapper.instance.remove(contextClosedEvent.getApplicationContext()); + Wrapper.instance.remove(contextClosedEvent.getApplicationContext()); } } /** - * The single {@link ContextStorage} wrapper that delegates to {@link EventPublisher} + * {@link ContextStorage#addWrapper(java.util.function.Function) Add} the + * {@link ContextStorage} wrapper to ensure that {@link EventPublisher} are propagated + * correctly. + */ + public static void addWrapper() { + if (isInstallable() && added.compareAndSet(false, true)) { + Wrapper.instance.addWrapper(); + } + } + + private static boolean isInstallable() { + return OTEL_CONTEXT_PRESENT && MICROMETER_OTEL_PRESENT; + } + + /** + * Single instance class used to add the wrapper and manage the {@link EventPublisher} * beans. */ - static class EventPublisherBeansContextWrapper implements UnaryOperator { + static final class Wrapper { - private static final AtomicBoolean added = new AtomicBoolean(); + static Wrapper instance = new Wrapper(); - private static final EventPublisherBeansContextWrapper instance = new EventPublisherBeansContextWrapper(); + private final MultiValueMap beans = new LinkedMultiValueMap<>(); - private final MultiValueMap publishers = new LinkedMultiValueMap<>(); + private volatile ContextStorage storageDelegate; - private volatile ContextStorage delegate; - - static void addWrapperIfNecessary() { - if (added.compareAndSet(false, true)) { - ContextStorage.addWrapper(instance); - } + private Wrapper() { } - @Override - public ContextStorage apply(ContextStorage contextStorage) { - return new EventPublisherBeansContextStorage(contextStorage); + private void addWrapper() { + ContextStorage.addWrapper(Storage::new); } void put(ApplicationContext applicationContext, List publishers) { synchronized (this) { - this.publishers.addAll(applicationContext, publishers); - this.delegate = null; + this.beans.addAll(applicationContext, publishers); + this.storageDelegate = null; } } void remove(ApplicationContext applicationContext) { synchronized (this) { - this.publishers.remove(applicationContext); - this.delegate = null; + this.beans.remove(applicationContext); + this.storageDelegate = null; } } - private ContextStorage getDelegate(ContextStorage parent) { - ContextStorage delegate = this.delegate; + ContextStorage getStorageDelegate(ContextStorage parent) { + ContextStorage delegate = this.storageDelegate; if (delegate == null) { synchronized (this) { delegate = parent; - for (List publishers : this.publishers.values()) { + for (List publishers : this.beans.values()) { for (EventPublishingContextWrapper publisher : publishers) { delegate = publisher.apply(delegate); } } + this.storageDelegate = delegate; } } return delegate; } /** - * The wrapped {@link ContextStorage} that delegates to the - * {@link EventPublisherBeansContextWrapper}. + * {@link ContextStorage} that delegates to the {@link EventPublisher} beans. */ - class EventPublisherBeansContextStorage implements ContextStorage { + class Storage implements ContextStorage { private final ContextStorage parent; - EventPublisherBeansContextStorage(ContextStorage wrapped) { - this.parent = wrapped; + Storage(ContextStorage parent) { + this.parent = parent; } @Override public Scope attach(Context toAttach) { - return getDelegate(this.parent).attach(toAttach); + return getDelegate().attach(toAttach); } @Override public Context current() { - return getDelegate(this.parent).current(); + return getDelegate().current(); } @Override public Context root() { - return getDelegate(this.parent).root(); + return getDelegate().root(); + } + + private ContextStorage getDelegate() { + return getStorageDelegate(this.parent); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansTestExecutionListener.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansTestExecutionListener.java new file mode 100644 index 0000000000..f364a6a9e2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublisherBeansTestExecutionListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2024 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 org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; + +/** + * JUnit {@link TestExecutionListener} to ensure + * {@link OpenTelemetryEventPublisherBeansApplicationListener#addWrapper()} is called as + * early as possible. + * + * @author Phillip Webb + * @since 3.4.0 + * @see OpenTelemetryEventPublisherBeansApplicationListener + */ +public class OpenTelemetryEventPublisherBeansTestExecutionListener implements TestExecutionListener { + + @Override + public void executionStarted(TestIdentifier testIdentifier) { + OpenTelemetryEventPublisherBeansApplicationListener.addWrapper(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 0000000000..284014dde2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansTestExecutionListener diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index f2ae3549ac..143d993355 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -13,4 +13,4 @@ org.springframework.boot.actuate.autoconfigure.tracing.LogCorrelationEnvironment # Application Listeners org.springframework.context.ApplicationListener=\ -org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener +org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansApplicationListener diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java index 6fa45e12f1..f7663b90f5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java @@ -31,7 +31,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.MDC; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener.EventPublisherBeansContextWrapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ForkedClassPath; @@ -58,7 +57,7 @@ class BaggagePropagationIntegrationTests { @BeforeEach @AfterEach void setup() { - EventPublisherBeansContextWrapper.addWrapperIfNecessary(); + OpenTelemetryEventPublisherBeansApplicationListener.addWrapper(); MDC.clear(); } @@ -291,7 +290,7 @@ class BaggagePropagationIntegrationTests { @Override public void initialize(ConfigurableApplicationContext applicationContext) { - applicationContext.addApplicationListener(new OpenTelemetryEventPublisherApplicationListener()); + applicationContext.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests.java new file mode 100644 index 0000000000..2daee77b4c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2024 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.lang.reflect.Method; +import java.util.List; +import java.util.function.Function; + +import io.opentelemetry.context.ContextStorage; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansApplicationListener.Wrapper.Storage; +import org.springframework.boot.testsupport.classpath.ForkedClassPath; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link OpenTelemetryEventPublisherBeansTestExecutionListener}. + * + * @author Phillip Webb + */ +@ForkedClassPath +class OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests { + + private final ContextStorage parent = mock(ContextStorage.class); + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void wrapperIsInstalled() throws Exception { + Class wrappersClass = Class.forName("io.opentelemetry.context.ContextStorageWrappers"); + Method getWrappersMethod = wrappersClass.getDeclaredMethod("getWrappers"); + getWrappersMethod.setAccessible(true); + List wrappers = (List) getWrappersMethod.invoke(null); + assertThat(wrappers).anyMatch((function) -> function.apply(this.parent) instanceof Storage); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryTracingAutoConfigurationTests.java index 87bed95c17..c38e2fc113 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryTracingAutoConfigurationTests.java @@ -54,14 +54,12 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener.EventPublisherBeansContextWrapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.annotation.Configurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -93,11 +91,6 @@ class OpenTelemetryTracingAutoConfigurationTests { org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class, OpenTelemetryTracingAutoConfiguration.class)); - @BeforeAll - static void addWrapper() { - EventPublisherBeansContextWrapper.addWrapperIfNecessary(); - } - @Test void shouldSupplyBeans() { this.contextRunner.run((context) -> { @@ -354,7 +347,7 @@ class OpenTelemetryTracingAutoConfigurationTests { } private void initializeOpenTelemetry(ConfigurableApplicationContext context) { - context.addApplicationListener(new OpenTelemetryEventPublisherApplicationListener()); + context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener()); Span.current(); }