diff --git a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle index 5f7e5d38bc..1119f2c521 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle @@ -59,6 +59,7 @@ dependencies { optional("org.apache.tomcat.embed:tomcat-embed-core") optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") + optional("io.micrometer:micrometer-tracing") testImplementation(project(":spring-boot-project:spring-boot-actuator")) testImplementation(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java index f2b3be1954..a809199e34 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java @@ -19,17 +19,36 @@ package org.springframework.boot.test.autoconfigure.actuate.observability; import java.util.List; import java.util.Objects; +import io.micrometer.tracing.Tracer; + +import org.springframework.aot.AotDetector; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.core.Ordered; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.util.ClassUtils; /** * {@link ContextCustomizerFactory} that globally disables metrics export and tracing * unless {@link AutoConfigureObservability} is set on the test class. + *
+ * Registers {@link Tracer#NOOP} if tracing is disabled, micrometer-tracing is on the
+ * classpath, and the user hasn't supplied their own {@link Tracer}.
*
* @author Chris Bono
* @author Moritz Halbritter
@@ -44,11 +63,10 @@ class ObservabilityContextCustomizerFactory implements ContextCustomizerFactory
if (annotation == null) {
return new DisableObservabilityContextCustomizer(true, true);
}
-
return new DisableObservabilityContextCustomizer(!annotation.metrics(), !annotation.tracing());
}
- static class DisableObservabilityContextCustomizer implements ContextCustomizer {
+ private static class DisableObservabilityContextCustomizer implements ContextCustomizer {
private final boolean disableMetrics;
@@ -68,9 +86,29 @@ class ObservabilityContextCustomizerFactory implements ContextCustomizerFactory
}
if (this.disableTracing) {
TestPropertyValues.of("management.tracing.enabled=false").applyTo(context);
+ registerNoopTracer(context);
}
}
+ private void registerNoopTracer(ConfigurableApplicationContext context) {
+ if (AotDetector.useGeneratedArtifacts()) {
+ return;
+ }
+ if (!ClassUtils.isPresent("io.micrometer.tracing.Tracer", context.getClassLoader())) {
+ return;
+ }
+ ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
+ if (beanFactory instanceof BeanDefinitionRegistry registry) {
+ registerNoopTracer(registry);
+ }
+ }
+
+ private void registerNoopTracer(BeanDefinitionRegistry registry) {
+ RootBeanDefinition definition = new RootBeanDefinition(NoopTracerRegistrar.class);
+ definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ registry.registerBeanDefinition(NoopTracerRegistrar.class.getName(), definition);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -90,4 +128,40 @@ class ObservabilityContextCustomizerFactory implements ContextCustomizerFactory
}
+ /**
+ * {@link BeanDefinitionRegistryPostProcessor} that runs after the
+ * {@link ConfigurationClassPostProcessor} and adds a {@link Tracer} bean definition
+ * when a {@link Tracer} hasn't already been registered.
+ */
+ private static class NoopTracerRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
+
+ private BeanFactory beanFactory;
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.LOWEST_PRECEDENCE;
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+ if (AotDetector.useGeneratedArtifacts()) {
+ return;
+ }
+ if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
+ Tracer.class, false, false).length == 0) {
+ registry.registerBeanDefinition("noopTracer", new RootBeanDefinition(Tracer.class, () -> Tracer.NOOP));
+ }
+ }
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ }
+
+ }
+
}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java
index 01f03ffcf9..f919b15022 100644
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java
+++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java
@@ -18,9 +18,17 @@ package org.springframework.boot.test.autoconfigure.actuate.observability;
import java.util.Collections;
+import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.ContextCustomizer;
@@ -39,53 +47,113 @@ class ObservabilityContextCustomizerFactoryTests {
@Test
void shouldDisableBothWhenNotAnnotated() {
- ContextCustomizer customizer = this.factory.createContextCustomizer(NoAnnotation.class,
- Collections.emptyList());
- assertThat(customizer).isNotNull();
+ ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
- customizer.customizeContext(context, null);
+ applyCustomizerToContext(customizer, context);
assertThatMetricsAreDisabled(context);
assertThatTracingIsDisabled(context);
}
@Test
void shouldDisableOnlyTracing() {
- ContextCustomizer customizer = this.factory.createContextCustomizer(OnlyMetrics.class, Collections.emptyList());
- assertThat(customizer).isNotNull();
+ ContextCustomizer customizer = createContextCustomizer(OnlyMetrics.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
- customizer.customizeContext(context, null);
+ applyCustomizerToContext(customizer, context);
assertThatMetricsAreEnabled(context);
assertThatTracingIsDisabled(context);
}
@Test
void shouldDisableOnlyMetrics() {
- ContextCustomizer customizer = this.factory.createContextCustomizer(OnlyTracing.class, Collections.emptyList());
- assertThat(customizer).isNotNull();
+ ContextCustomizer customizer = createContextCustomizer(OnlyTracing.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
- customizer.customizeContext(context, null);
+ applyCustomizerToContext(customizer, context);
assertThatMetricsAreDisabled(context);
assertThatTracingIsEnabled(context);
}
@Test
void shouldEnableBothWhenAnnotated() {
- ContextCustomizer customizer = this.factory.createContextCustomizer(WithAnnotation.class,
- Collections.emptyList());
- assertThat(customizer).isNotNull();
+ ContextCustomizer customizer = createContextCustomizer(WithAnnotation.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
- customizer.customizeContext(context, null);
+ applyCustomizerToContext(customizer, context);
assertThatMetricsAreEnabled(context);
assertThatTracingIsEnabled(context);
}
+ @Test
+ void shouldRegisterNoopTracerIfTracingIsDisabled() {
+ ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
+ ConfigurableApplicationContext context = new GenericApplicationContext();
+ applyCustomizerToContext(customizer, context);
+ context.refresh();
+ Tracer tracer = context.getBean(Tracer.class);
+ assertThat(tracer).isNotNull();
+ assertThat(tracer.nextSpan().isNoop()).isTrue();
+ }
+
+ @Test
+ void shouldNotRegisterNoopTracerIfTracingIsEnabled() {
+ ContextCustomizer customizer = createContextCustomizer(WithAnnotation.class);
+ ConfigurableApplicationContext context = new GenericApplicationContext();
+ applyCustomizerToContext(customizer, context);
+ context.refresh();
+ assertThat(context.getBeanProvider(Tracer.class).getIfAvailable()).as("Tracer bean").isNull();
+ }
+
+ @Test
+ void shouldNotRegisterNoopTracerIfMicrometerTracingIsNotPresent() throws Exception {
+ try (FilteredClassLoader filteredClassLoader = new FilteredClassLoader("io.micrometer.tracing")) {
+ ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
+ new ApplicationContextRunner().withClassLoader(filteredClassLoader)
+ .withInitializer(applyCustomizer(customizer)).run((context) -> {
+ assertThat(context).doesNotHaveBean(Tracer.class);
+ assertThatMetricsAreDisabled(context);
+ assertThatTracingIsDisabled(context);
+ });
+ }
+ }
+
+ @Test
+ void shouldBackOffOnCustomTracer() {
+ ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
+ new ApplicationContextRunner().withConfiguration(UserConfigurations.of(CustomTracer.class))
+ .withInitializer(applyCustomizer(customizer)).run((context) -> {
+ assertThat(context).hasSingleBean(Tracer.class);
+ assertThat(context).hasBean("customTracer");
+ });
+ }
+
+ @Test
+ void shouldNotRunIfAotIsEnabled() {
+ ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
+ new ApplicationContextRunner().withSystemProperties("spring.aot.enabled:true")
+ .withInitializer(applyCustomizer(customizer))
+ .run((context) -> assertThat(context).doesNotHaveBean(Tracer.class));
+ }
+
@Test
void hashCodeAndEquals() {
- ContextCustomizer customizer1 = this.factory.createContextCustomizer(OnlyMetrics.class, null);
- ContextCustomizer customizer2 = this.factory.createContextCustomizer(OnlyTracing.class, null);
+ ContextCustomizer customizer1 = createContextCustomizer(OnlyMetrics.class);
+ ContextCustomizer customizer2 = createContextCustomizer(OnlyTracing.class);
assertThat(customizer1).isNotEqualTo(customizer2);
}
+ private void applyCustomizerToContext(ContextCustomizer customizer, ConfigurableApplicationContext context) {
+ customizer.customizeContext(context, null);
+ }
+
+ private ContextCustomizer createContextCustomizer(Class> testClass) {
+ ContextCustomizer contextCustomizer = this.factory.createContextCustomizer(testClass, Collections.emptyList());
+ assertThat(contextCustomizer).as("contextCustomizer").isNotNull();
+ return contextCustomizer;
+ }
+
+ private ApplicationContextInitializer