Polish "Disable Micrometer global registry in tests by default"

See gh-51142
This commit is contained in:
Moritz Halbritter
2026-07-29 11:37:35 +02:00
parent c66a5ef94f
commit be35ff4149
10 changed files with 224 additions and 200 deletions
@@ -1,77 +0,0 @@
/*
* Copyright 2012-present 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.test.metrics;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
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.util.ClassUtils;
/**
* {@link ContextCustomizerFactory} to disable the use of Micrometer's
* {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in tests,
* preventing {@link io.micrometer.core.instrument.MeterRegistry meter registries} from
* pinning application contexts when many test contexts are cached.
*
* @author Lordwill Kandiro
*/
class DisableGlobalMeterRegistryContextCustomizerFactory implements ContextCustomizerFactory {
private static final String METRICS_CLASS = "io.micrometer.core.instrument.Metrics";
private static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry";
@Override
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
if (ClassUtils.isPresent(METRICS_CLASS, testClass.getClassLoader())) {
return new DisableGlobalMeterRegistryContextCustomizer();
}
return null;
}
static final class DisableGlobalMeterRegistryContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ConfigurableEnvironment environment = context.getEnvironment();
if (environment.getProperty(USE_GLOBAL_REGISTRY_PROPERTY) == null) {
TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(environment);
}
}
@Override
public boolean equals(Object obj) {
return (obj instanceof DisableGlobalMeterRegistryContextCustomizer);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
}
@@ -1,23 +0,0 @@
/*
* Copyright 2012-present 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.
*/
/**
* Spring Boot support for metrics testing.
*/
@NullMarked
package org.springframework.boot.test.metrics;
import org.jspecify.annotations.NullMarked;
@@ -5,8 +5,7 @@ org.springframework.boot.test.context.PropertyMappingContextCustomizerFactory,\
org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\
org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactory,\
org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory,\
org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\
org.springframework.boot.test.metrics.DisableGlobalMeterRegistryContextCustomizerFactory
org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
@@ -1,90 +0,0 @@
/*
* Copyright 2012-present 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.test.metrics;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DisableGlobalMeterRegistryContextCustomizerFactory}.
*
* @author Lordwill Kandiro
*/
class DisableGlobalMeterRegistryContextCustomizerFactoryTests {
private final DisableGlobalMeterRegistryContextCustomizerFactory factory = new DisableGlobalMeterRegistryContextCustomizerFactory();
@Test
void createContextCustomizerReturnsNullWhenMetricsIsNotPresent() {
assertThat(this.factory.createContextCustomizer(getClass(), Collections.emptyList())).isNull();
}
@Test
void disablesGlobalMeterRegistryByDefault() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.setEnvironment(new StandardEnvironment());
new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer()
.customizeContext(context, mock(MergedContextConfiguration.class));
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry"))
.isEqualTo("false");
}
}
@Test
void doesNotOverrideExplicitTrue() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
StandardEnvironment environment = new StandardEnvironment();
TestPropertyValues.of("management.metrics.use-global-registry=true").applyTo(environment);
context.setEnvironment(environment);
new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer()
.customizeContext(context, mock(MergedContextConfiguration.class));
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry"))
.isEqualTo("true");
}
}
@Test
void doesNotOverrideExplicitFalse() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
StandardEnvironment environment = new StandardEnvironment();
TestPropertyValues.of("management.metrics.use-global-registry=false").applyTo(environment);
context.setEnvironment(environment);
new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer()
.customizeContext(context, mock(MergedContextConfiguration.class));
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry"))
.isEqualTo("false");
}
}
@Test
void equalsAndHashCode() {
ContextCustomizer customizer1 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer();
ContextCustomizer customizer2 = new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer();
assertThat(customizer1).isEqualTo(customizer2).hasSameHashCodeAs(customizer2);
}
}
@@ -64,6 +64,9 @@ management:
use-global-registry: false
----
NOTE: In tests that use `spring-boot-micrometer-metrics-test`, the global registry is disabled by default.
If you want to use the global registry in such a test, annotate it with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] using `useGlobalRegistry = true`, or set the configprop:management.metrics.use-global-registry[] property explicitly to `true`.
You can register any number of javadoc:org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryCustomizer[] beans to further configure the registry, such as applying common tags, before any meters are registered with the registry:
include-code::commontags/MyMeterRegistryConfiguration[]
@@ -244,6 +244,9 @@ If you need to export metrics to a different backend as part of an integration t
If you annotate xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.autoconfigured-tests[a sliced test] with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation], it auto-configures an in-memory javadoc:io.micrometer.core.instrument.MeterRegistry[].
Data exporting in sliced tests is not supported with the javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] annotation.
Meter registries are not added to the xref:actuator/metrics.adoc#actuator.metrics.getting-started[global registry] by default in tests.
If you need the global registry in a test, annotate it with javadoc:org.springframework.boot.micrometer.metrics.test.autoconfigure.AutoConfigureMetrics[format=annotation] using `useGlobalRegistry = true`, or set the configprop:management.metrics.use-global-registry[] property explicitly to `true`.
[[testing.spring-boot-applications.tracing]]
@@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
* an {@link ObservationRegistry} are added to the application context.
*
* @author Moritz Halbritter
* @author Lordwill Kandiro
* @since 4.0.0
*/
@Target(ElementType.TYPE)
@@ -51,4 +52,14 @@ public @interface AutoConfigureMetrics {
*/
boolean export() default true;
/**
* Whether {@link MeterRegistry meter registries} should be added to Micrometer's
* {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in the
* test. If {@code management.metrics.use-global-registry} has been set explicitly,
* its value is used and this attribute has no effect.
* @return whether the global registry should be used in the test
* @since 4.2.0
*/
boolean useGlobalRegistry() default false;
}
@@ -31,43 +31,52 @@ import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* {@link ContextCustomizerFactory} that globally disables metrics export in tests. The
* behaviour can be controlled with {@link AutoConfigureMetrics} on the test class or via
* the {@value #AUTO_CONFIGURE_PROPERTY} property.
* {@link ContextCustomizerFactory} that globally disables metrics export and the use of
* Micrometer's {@link io.micrometer.core.instrument.Metrics#globalRegistry global
* registry} in tests. The behaviour can be controlled with {@link AutoConfigureMetrics}
* on the test class, via the {@value #AUTO_CONFIGURE_PROPERTY} property and via the
* {@value #USE_GLOBAL_REGISTRY_PROPERTY} property.
*
* @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Lordwill Kandiro
*/
class MetricsContextCustomizerFactory implements ContextCustomizerFactory {
static final String AUTO_CONFIGURE_PROPERTY = "spring.test.metrics.export";
static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry";
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
AutoConfigureMetrics annotation = TestContextAnnotationUtils.findMergedAnnotation(testClass,
AutoConfigureMetrics.class);
return new DisableMetricsExportContextCustomizer(annotation);
return new MetricsContextCustomizer(annotation);
}
private static class DisableMetricsExportContextCustomizer implements ContextCustomizer {
private static class MetricsContextCustomizer implements ContextCustomizer {
private final @Nullable AutoConfigureMetrics annotation;
DisableMetricsExportContextCustomizer(@Nullable AutoConfigureMetrics annotation) {
MetricsContextCustomizer(@Nullable AutoConfigureMetrics annotation) {
this.annotation = annotation;
}
@Override
public void customizeContext(ConfigurableApplicationContext context,
MergedContextConfiguration mergedContextConfiguration) {
if (areMetricsDisabled(context.getEnvironment())) {
Environment environment = context.getEnvironment();
if (areMetricsDisabled(environment)) {
TestPropertyValues
.of("management.defaults.metrics.export.enabled=false",
"management.simple.metrics.export.enabled=true")
.applyTo(context);
}
if (isGlobalRegistryDisabled(environment)) {
TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(context);
}
}
private boolean areMetricsDisabled(Environment environment) {
@@ -77,6 +86,13 @@ class MetricsContextCustomizerFactory implements ContextCustomizerFactory {
return !environment.getProperty(AUTO_CONFIGURE_PROPERTY, Boolean.class, false);
}
private boolean isGlobalRegistryDisabled(Environment environment) {
if (this.annotation != null && this.annotation.useGlobalRegistry()) {
return false;
}
return !environment.containsProperty(USE_GLOBAL_REGISTRY_PROPERTY);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
@@ -85,7 +101,7 @@ class MetricsContextCustomizerFactory implements ContextCustomizerFactory {
if (o == null || getClass() != o.getClass()) {
return false;
}
DisableMetricsExportContextCustomizer that = (DisableMetricsExportContextCustomizer) o;
MetricsContextCustomizer that = (MetricsContextCustomizer) o;
return Objects.equals(this.annotation, that.annotation);
}
@@ -0,0 +1,92 @@
/*
* Copyright 2012-present 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.micrometer.metrics.test.autoconfigure;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MetricsContextCustomizerFactory} in a real Spring test
* context, verifying that auto-configured {@link MeterRegistry meter registries} are kept
* out of {@link Metrics#globalRegistry} by default and can be opted back in with
* {@link AutoConfigureMetrics @AutoConfigureMetrics} or the
* {@code management.metrics.use-global-registry} property.
*
* @author Moritz Halbritter
* @author Lordwill Kandiro
*/
class GlobalMeterRegistryIntegrationTests {
@SpringJUnitConfig
@Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@DirtiesContext
static class WhenNotAnnotatedTests {
@Autowired
private MeterRegistry meterRegistry;
@Test
void meterRegistryIsNotAddedToTheGlobalRegistry() {
assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.meterRegistry);
}
}
@SpringJUnitConfig
@Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@AutoConfigureMetrics(useGlobalRegistry = true)
@DirtiesContext
static class WhenAnnotatedWithTrueUseGlobalRegistryAttributeTests {
@Autowired
private MeterRegistry meterRegistry;
@Test
void meterRegistryIsAddedToTheGlobalRegistry() {
assertThat(Metrics.globalRegistry.getRegistries()).contains(this.meterRegistry);
}
}
@SpringJUnitConfig
@Import({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@TestPropertySource(properties = "management.metrics.use-global-registry=true")
@DirtiesContext
static class WhenPropertyIsExplicitlyEnabledTests {
@Autowired
private MeterRegistry meterRegistry;
@Test
void meterRegistryIsAddedToTheGlobalRegistry() {
assertThat(Metrics.globalRegistry.getRegistries()).contains(this.meterRegistry);
}
}
}
@@ -74,6 +74,71 @@ class MetricsContextCustomizerFactoryTests {
assertThatMetricsAreEnabled(context);
}
@Test
void whenNotAnnotatedGlobalRegistryIsDisabled() {
ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
applyCustomizerToContext(customizer, context);
assertThatGlobalRegistryIsDisabled(context);
}
@Test
void whenAnnotatedWithDefaultAttributeGlobalRegistryIsDisabled() {
ContextCustomizer customizer = createContextCustomizer(MetricsExportDefault.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
applyCustomizerToContext(customizer, context);
assertThatGlobalRegistryIsDisabled(context);
}
@Test
void whenAnnotatedWithFalseUseGlobalRegistryAttributeGlobalRegistryIsDisabled() {
ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryDisabled.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
applyCustomizerToContext(customizer, context);
assertThatGlobalRegistryIsDisabled(context);
}
@Test
void whenAnnotatedWithTrueUseGlobalRegistryAttributeGlobalRegistryIsEnabled() {
ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryEnabled.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
applyCustomizerToContext(customizer, context);
assertThatGlobalRegistryIsNotConfigured(context);
}
@Test
void globalRegistryPropertySetToTrueIsNotOverridden() {
ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("management.metrics.use-global-registry", "true");
context.setEnvironment(environment);
applyCustomizerToContext(customizer, context);
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("true");
}
@Test
void globalRegistryPropertySetToFalseIsNotOverridden() {
ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("management.metrics.use-global-registry", "false");
context.setEnvironment(environment);
applyCustomizerToContext(customizer, context);
assertThatGlobalRegistryIsDisabled(context);
}
@Test
void annotationDoesNotTakePrecedenceOverGlobalRegistryProperty() {
ContextCustomizer customizer = createContextCustomizer(UseGlobalRegistryDisabled.class);
ConfigurableApplicationContext context = new GenericApplicationContext();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("management.metrics.use-global-registry", "true");
context.setEnvironment(environment);
applyCustomizerToContext(customizer, context);
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("true");
}
@Test
void notEquals() {
ContextCustomizer customizer1 = createContextCustomizer(MetricsExportEnabled.class);
@@ -81,6 +146,13 @@ class MetricsContextCustomizerFactoryTests {
assertThat(customizer1).isNotEqualTo(customizer2);
}
@Test
void notEqualsWhenUseGlobalRegistryDiffers() {
ContextCustomizer customizer1 = createContextCustomizer(UseGlobalRegistryEnabled.class);
ContextCustomizer customizer2 = createContextCustomizer(UseGlobalRegistryDisabled.class);
assertThat(customizer1).isNotEqualTo(customizer2);
}
@Test
void equals() {
ContextCustomizer customizer1 = createContextCustomizer(MetricsExportEnabled.class);
@@ -155,6 +227,14 @@ class MetricsContextCustomizerFactoryTests {
assertThat(context.getEnvironment().getProperty("management.simple.metrics.export.enabled")).isNull();
}
private void assertThatGlobalRegistryIsDisabled(ConfigurableApplicationContext context) {
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isEqualTo("false");
}
private void assertThatGlobalRegistryIsNotConfigured(ConfigurableApplicationContext context) {
assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")).isNull();
}
static class NoAnnotation {
}
@@ -174,4 +254,14 @@ class MetricsContextCustomizerFactoryTests {
}
@AutoConfigureMetrics(useGlobalRegistry = false)
static class UseGlobalRegistryDisabled {
}
@AutoConfigureMetrics(useGlobalRegistry = true)
static class UseGlobalRegistryEnabled {
}
}