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 applyCustomizer( + ContextCustomizer customizer) { + return (applicationContext) -> customizer.customizeContext(applicationContext, null); + } + private void assertThatTracingIsDisabled(ConfigurableApplicationContext context) { assertThat(context.getEnvironment().getProperty("management.tracing.enabled")).isEqualTo("false"); } @@ -124,4 +192,14 @@ class ObservabilityContextCustomizerFactoryTests { } + @Configuration(proxyBeanMethods = false) + static class CustomTracer { + + @Bean + Tracer customTracer() { + return Mockito.mock(Tracer.class); + } + + } + } diff --git a/src/checkstyle/import-control.xml b/src/checkstyle/import-control.xml index 089b4371a9..d0e94c7753 100644 --- a/src/checkstyle/import-control.xml +++ b/src/checkstyle/import-control.xml @@ -80,6 +80,14 @@ + + + + + + + +