Add noop tracer bean if tracing is disabled

The new functionality doesn't expose any public API
and works the same as WebTestClientContextCustomizer
does.

Closes gh-32907
This commit is contained in:
Moritz Halbritter
2022-11-02 12:07:01 +01:00
parent 7aff82009f
commit 85fd475821
4 changed files with 179 additions and 18 deletions

View File

@@ -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"))

View File

@@ -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.
* <p>
* 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 {
}
}
}

View File

@@ -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<ConfigurableApplicationContext> 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);
}
}
}

View File

@@ -80,6 +80,14 @@
<disallow pkg="org.springframework.boot.context" />
</subpackage>
<subpackage name="test">
<subpackage name="autoconfigure">
<subpackage name="actuate">
<allow pkg="io.micrometer" />
</subpackage>
</subpackage>
</subpackage>
<!-- Web related concerns -->
<subpackage name="web">